import os
import sys
import json
import shutil
import jinja2
import getpass
import subprocess as sp

from PySide2.QtWidgets import *
from PySide2.QtCore import Qt, QSize
from PySide2.QtGui import QFont, QDoubleValidator, QIntValidator

from CustStyleSheets import *
from CustWidgets import LineLabel, KeyLabel
from CustWidgets import ClickableWidget, TypeSelectorDialog, CircleButton
from CustWidgets import CameraTuner, RtspCameraTuner, UsbCameraTuner
from utils import EII_MODULE, EII_FILE_NAME, get_random_server_port, get_random_msgbus_port, get_random_tcp_output_port, get_eiigui_root_path
from utils import copy_files_under_dir
from utils.eii_util import get_eiigui_root_path

class ConfigWgt(QWidget):
    """Collect user input data about EII services
    and send it to the next stage.
    """

    def __init__(self, eii_home, project_name, parent=None, host_pwd=None):
        super(ConfigWgt, self).__init__(parent=parent)
        self.eii_home = eii_home
        self.host_pwd = host_pwd
        self.project_name = project_name
        self.project_path = os.path.join(self.eii_home, 
            "eii_helper/workspace", self.project_name)
        self.ingestion_app_name = self.project_name + "_video_ingestion"
        self.analytics_app_name = self.project_name + "_video_analytics"
        self.ingestion_topic_name = self.project_name + "_camera1_stream"
        self.analytics_topic_name = self.project_name + "_camera1_stream_results"
        # NOTE: make sure the etcd server runs on the same machine
        self.eii_etcd_host = "127.0.0.1"
        # For tracking udfs source folders
        self.ingestion_udfs_path = ""
        self.analytics_udfs_path = ""
        # For tracking udfs labels contents
        self.udfs_labels = ""
        
        # vi_va or only_vi
        self.vi_va_mode = "vi_va"
        
        self.init_ui()

        self.all_clickable_wgts = [
            self.click_wgt_ingestion,
            self.click_wgt_analytics,
            self.click_wgt_image_store,
        ]

        self.static_signals_collector()

    def init_ui(self):
        self.grid_lyt_global = QGridLayout(self)
        self.grid_lyt_global.setContentsMargins(0, 0, 0, 0)

        self.init_switch_mode()
        self.init_data_stream_name()
        self.apply_existing_config()

    def init_switch_mode(self):
        self.wgt_switch_mode = QWidget(self)
        self.grid_lyt_switch_mode = QGridLayout(self.wgt_switch_mode)
        
        self.lbl_switch_mode = QLabel(self.wgt_switch_mode)
        self.lbl_switch_mode.setText("Switch Mode")
        self.lbl_switch_mode.setStyleSheet("color: rgb(32, 74, 135);")
        self.lbl_switch_mode.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter)
        self.grid_lyt_switch_mode.addWidget(self.lbl_switch_mode, 1, 0, 1, 1)

        wgt_switch_mode_btns = QWidget(self.wgt_switch_mode)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(wgt_switch_mode_btns.sizePolicy().hasHeightForWidth())
        wgt_switch_mode_btns.setSizePolicy(sizePolicy)
        wgt_switch_mode_btns.setMinimumSize(QSize(100, 0))
        vbox_lyt_wgt_switch_mode_btns = QVBoxLayout(wgt_switch_mode_btns)
        vbox_lyt_wgt_switch_mode_btns.setContentsMargins(0, 0, 0, 0)
        vbox_lyt_wgt_switch_mode_btns.setSpacing(0)

        self.btn_vi_va = QPushButton(wgt_switch_mode_btns)
        self.btn_vi_va.setText("VI + VA")
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.btn_vi_va.sizePolicy().hasHeightForWidth())
        self.btn_vi_va.setSizePolicy(sizePolicy)
        self.btn_vi_va.setMinimumSize(QSize(100, 35))
        self.btn_vi_va.setMaximumSize(QSize(100, 35))
        self.btn_vi_va.setStyleSheet(SS_STD_BTN_ENABLE)
        vbox_lyt_wgt_switch_mode_btns.addWidget(self.btn_vi_va)

        self.btn_only_vi = QPushButton(wgt_switch_mode_btns)
        self.btn_only_vi.setText("VI")
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.btn_only_vi.sizePolicy().hasHeightForWidth())
        self.btn_only_vi.setSizePolicy(sizePolicy)
        self.btn_only_vi.setMinimumSize(QSize(100, 35))
        self.btn_only_vi.setMaximumSize(QSize(100, 35))
        self.btn_only_vi.setStyleSheet(SS_STD_BTN_DISABLE)
        vbox_lyt_wgt_switch_mode_btns.addWidget(self.btn_only_vi)

        self.grid_lyt_switch_mode.addWidget(wgt_switch_mode_btns, 2, 0, 1, 1)

        spacer1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_switch_mode.addItem(spacer1, 0, 0, 1, 1)
        spacer2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_switch_mode.addItem(spacer2, 3, 0, 1, 1)

        self.grid_lyt_global.addWidget(self.wgt_switch_mode, 0, 0, 1, 1)

    def init_data_stream_name(self):
        self.wgt_data_stream_name = QWidget(self)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.wgt_data_stream_name.sizePolicy().hasHeightForWidth())
        self.wgt_data_stream_name.setSizePolicy(sizePolicy)
        self.grid_lyt_data_stream_name = QGridLayout(self.wgt_data_stream_name)

        self.lbl_data_stream_name = QLabel(self.wgt_data_stream_name)
        self.lbl_data_stream_name.setText(self.project_name)
        font = QFont()
        font.setUnderline(True)
        self.lbl_data_stream_name.setFont(font)
        self.lbl_data_stream_name.setStyleSheet("color: rgb(32, 74, 135);")
        self.grid_lyt_data_stream_name.addWidget(self.lbl_data_stream_name, 0, 0, 1, 1)
        
        self.wgt_main_inside = QWidget(self.wgt_data_stream_name)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.wgt_main_inside.sizePolicy().hasHeightForWidth())
        self.wgt_main_inside.setSizePolicy(sizePolicy)
        self.wgt_main_inside.setObjectName("wgt_main_inside")
        wgt_main_inside_style = \
        """
        QWidget#wgt_main_inside 
        {
            border: 2px groove gray;
            border-color: rgb(85, 87, 83);
        }
        """
        self.wgt_main_inside.setStyleSheet(wgt_main_inside_style)
        self.grid_lyt_main_inside = QGridLayout(self.wgt_main_inside)
        self.grid_lyt_main_inside.setContentsMargins(0, 0, 0, 0)

        self.init_comp_layout()
        self.init_configuration()

        self.line_central = QFrame(self.wgt_main_inside)
        self.line_central.setStyleSheet("border: 1px groove gray; background-color: rgb(238, 238, 236);")
        self.line_central.setFrameShape(QFrame.VLine)
        self.line_central.setFrameShadow(QFrame.Sunken)
        self.grid_lyt_main_inside.addWidget(self.line_central, 0, 1, 1, 1)

        self.grid_lyt_data_stream_name.addWidget(self.wgt_main_inside, 1, 0, 1, 1)

        self.grid_lyt_global.addWidget(self.wgt_data_stream_name, 0, 1, 1, 1)

    def init_comp_layout(self):
        self.wgt_comp_layout = QWidget(self.wgt_main_inside)
        self.grid_lyt_comp_layout = QGridLayout(self.wgt_comp_layout)
        self.grid_lyt_comp_layout.setVerticalSpacing(0)
        self.grid_lyt_comp_layout.setHorizontalSpacing(40)

        self.lbl_comp_layout = QLabel(self.wgt_comp_layout)
        self.lbl_comp_layout.setText("Components Layout")
        self.lbl_comp_layout.setAlignment(Qt.AlignCenter)
        self.grid_lyt_comp_layout.addWidget(self.lbl_comp_layout, 0, 0, 1, 6)

        # --------------------------- Video Ingestion --------------------------- #
        # Clickable widget
        self.click_wgt_ingestion = ClickableWidget(self.wgt_comp_layout)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.click_wgt_ingestion.sizePolicy().hasHeightForWidth())
        self.click_wgt_ingestion.setSizePolicy(sizePolicy)
        self.click_wgt_ingestion.setMinimumSize(QSize(180, 80))
        self.click_wgt_ingestion.setMaximumSize(QSize(180, 80))
        self.click_wgt_ingestion.setStyleSheet(SS_BIG_BTN_ENABLE)
        grid_lyt_click_wgt_ingestion = QGridLayout(self.click_wgt_ingestion)
        grid_lyt_click_wgt_ingestion.setContentsMargins(0, 0, 0, 0)
        grid_lyt_click_wgt_ingestion.setSpacing(0)
        self.lbl_ingestion = QLabel(self.click_wgt_ingestion)
        self.lbl_ingestion.setText("Video Ingestion")
        font = QFont()
        font.setPointSize(14)
        self.lbl_ingestion.setFont(font)
        self.lbl_ingestion.setAlignment(Qt.AlignCenter)
        grid_lyt_click_wgt_ingestion.addWidget(self.lbl_ingestion, 0, 0, 1, 1)
        self.grid_lyt_comp_layout.addWidget(self.click_wgt_ingestion, 2, 1, 1, 4, Qt.AlignHCenter)
        
        # --------------------------- Video Analytics --------------------------- #
        # Clickable widget
        self.click_wgt_analytics = ClickableWidget(self.wgt_comp_layout)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.click_wgt_analytics.sizePolicy().hasHeightForWidth())
        self.click_wgt_analytics.setSizePolicy(sizePolicy)
        self.click_wgt_analytics.setMinimumSize(QSize(180, 80))
        self.click_wgt_analytics.setMaximumSize(QSize(180, 80))
        self.click_wgt_analytics.setStyleSheet(SS_BIG_BTN_DISABLE)
        grid_lyt_click_wgt_analytics = QGridLayout(self.click_wgt_analytics)
        grid_lyt_click_wgt_analytics.setContentsMargins(0, 0, 0, 0)
        grid_lyt_click_wgt_analytics.setSpacing(0)
        self.lbl_analytics = QLabel(self.click_wgt_analytics)
        self.lbl_analytics.setText("Video Analytics")
        font = QFont()
        font.setPointSize(14)
        self.lbl_analytics.setFont(font)
        self.lbl_analytics.setAlignment(Qt.AlignCenter)
        grid_lyt_click_wgt_analytics.addWidget(self.lbl_analytics, 0, 0, 1, 1)
        self.grid_lyt_comp_layout.addWidget(self.click_wgt_analytics, 4, 1, 1, 4, Qt.AlignHCenter)
        
        # --------------------------- Image Store --------------------------- #
        # Clickable widget
        self.click_wgt_image_store = ClickableWidget(self.wgt_comp_layout)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.click_wgt_image_store.sizePolicy().hasHeightForWidth())
        self.click_wgt_image_store.setSizePolicy(sizePolicy)
        self.click_wgt_image_store.setMinimumSize(QSize(180, 80))
        self.click_wgt_image_store.setMaximumSize(QSize(180, 80))
        self.click_wgt_image_store.setObjectName("click_widget")
        self.click_wgt_image_store.setStyleSheet(SS_BIG_BTN_DISABLE)
        grid_lyt_click_wgt_image_store = QGridLayout(self.click_wgt_image_store)
        grid_lyt_click_wgt_image_store.setContentsMargins(9, 0, 0, 0)
        grid_lyt_click_wgt_image_store.setSpacing(0)
        self.lbl_image_store = QLabel(self.click_wgt_analytics)
        self.lbl_image_store.setText("Output Modules")
        font = QFont()
        font.setPointSize(14)
        self.lbl_image_store.setFont(font)
        self.lbl_image_store.setAlignment(Qt.AlignCenter)
        grid_lyt_click_wgt_image_store.addWidget(self.lbl_image_store, 0, 0, 1, 1)
        self.grid_lyt_comp_layout.addWidget(self.click_wgt_image_store, 6, 1, 1, 4, Qt.AlignHCenter)
        
        # Line between ingestion and analytics
        self.line_ingestion_analytics = LineLabel(self.wgt_comp_layout, pt1="1", pt2="5")
        self.line_ingestion_analytics.setMinimumSize(QSize(0, 45))
        self.line_ingestion_analytics.setMaximumSize(QSize(16777215, 45))
        self.grid_lyt_comp_layout.addWidget(self.line_ingestion_analytics, 3, 1, 1, 4)
        
        # Line between analytics and image store
        # line_analytics_imagestore = LineLabel(self.wgt_comp_layout, pt1="2", pt2="5")
        # line_analytics_imagestore.setMinimumSize(QSize(0, 45))
        # line_analytics_imagestore.setMaximumSize(QSize(16777215, 45))
        # self.grid_lyt_comp_layout.addWidget(line_analytics_imagestore, 5, 0, 1, 3)
        
        # Line between analytics and output module
        line_analytics_outputmodule = LineLabel(self.wgt_comp_layout, pt1="1", pt2="5")
        line_analytics_outputmodule.setMinimumSize(QSize(0, 45))
        line_analytics_outputmodule.setMaximumSize(QSize(16777215, 45))
        self.grid_lyt_comp_layout.addWidget(line_analytics_outputmodule, 5, 1, 1, 4)
        
        # Spacers
        spacer1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_comp_layout.addItem(spacer1, 1, 0, 1, 1)
        spacer2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_comp_layout.addItem(spacer2, 7, 0, 1, 1)
        
        self.grid_lyt_main_inside.addWidget(self.wgt_comp_layout, 0, 0, 1, 1)

    def init_configuration(self):
        self.init_configuration_ingestion()
        self.init_configuration_analytics()
        self.init_configuration_output_modules()
        self.wgt_cfg_ingestion.show()
        self.wgt_cfg_analytics.hide()
        self.wgt_cfg_output_modules.hide()
        pass
    
    def init_configuration_ingestion(self):
        self.wgt_cfg_ingestion = QWidget(self.wgt_main_inside)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.wgt_cfg_ingestion.sizePolicy().hasHeightForWidth())
        self.wgt_cfg_ingestion.setSizePolicy(sizePolicy)
        self.grid_lyt_cfg_ingestion = QGridLayout(self.wgt_cfg_ingestion)
        
        self.lbl_cfg_ingestion = QLabel(self.wgt_cfg_ingestion)
        self.lbl_cfg_ingestion.setText("Configuration")
        self.lbl_cfg_ingestion.setAlignment(Qt.AlignCenter)
        self.grid_lyt_cfg_ingestion.addWidget(self.lbl_cfg_ingestion, 0, 0, 1, 1)
        
        self.tab_wgt_cfg_ingestion = QTabWidget(self.wgt_cfg_ingestion)
        self.grid_lyt_cfg_ingestion.addWidget(self.tab_wgt_cfg_ingestion, 1, 0, 1, 1)
        
        # --------------------------- Ingestor --------------------------- #
        self.tab_cfg_ingestion_ingestor = QWidget()
        self.grid_lyt_cfg_ingestion_ingestor = QGridLayout(self.tab_cfg_ingestion_ingestor)
        self.tab_wgt_cfg_ingestion.addTab(self.tab_cfg_ingestion_ingestor, "Ingestor")
        
        # Source
        self.lbl_ingestion_ingestor_source = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_source.setText("Source")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_source, 0, 1, 1, 1)
        self.cbbox_ingestion_ingestor_source = QComboBox(self.tab_cfg_ingestion_ingestor)
        self.cbbox_ingestion_ingestor_source.addItem("Video File")
        self.cbbox_ingestion_ingestor_source.addItem("2D Camera")
        self.cbbox_ingestion_ingestor_source.addItem("3D Camera")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.cbbox_ingestion_ingestor_source, 0, 2, 1, 1)
        self.ckbox_ingestion_ingestor_loop = QCheckBox(self.tab_cfg_ingestion_ingestor)
        self.ckbox_ingestion_ingestor_loop.setChecked(True)
        self.ckbox_ingestion_ingestor_loop.setText("Loop")
        self.ckbox_ingestion_ingestor_loop.setMinimumSize(QSize(65, 16777215))
        self.ckbox_ingestion_ingestor_loop.setMaximumSize(QSize(65, 16777215))
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.ckbox_ingestion_ingestor_loop, 0, 3, 1, 1, Qt.AlignHCenter)

        # When source is video file
        self.lbl_ingestion_ingestor_video_file = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_video_file.setText("Video File")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_video_file, 1, 1, 1, 1)
        self.le_ingestion_ingestor_video_file = QLineEdit(self.tab_cfg_ingestion_ingestor)
        self.le_ingestion_ingestor_video_file.setReadOnly(True)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.le_ingestion_ingestor_video_file, 1, 2, 1, 1)
        self.btn_ingestion_ingestor_open_video_file = QPushButton(self.tab_cfg_ingestion_ingestor)
        self.btn_ingestion_ingestor_open_video_file.setText("...")
        self.btn_ingestion_ingestor_open_video_file.setMinimumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_open_video_file.setMaximumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_open_video_file.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.btn_ingestion_ingestor_open_video_file, 1, 3, 1, 1, Qt.AlignHCenter|Qt.AlignVCenter)
        
        # When source is 2D camera
        self.lbl_ingestion_ingestor_2d_camera_type = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_2d_camera_type.setText("Camera Type")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_2d_camera_type, 2, 1, 1, 1)
        self.cbbox_ingestion_ingestor_2d_camera_type = QComboBox(self.tab_cfg_ingestion_ingestor)
        self.cbbox_ingestion_ingestor_2d_camera_type.addItem("GigE")
        self.cbbox_ingestion_ingestor_2d_camera_type.addItem("USB")
        self.cbbox_ingestion_ingestor_2d_camera_type.addItem("RTSP")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.cbbox_ingestion_ingestor_2d_camera_type, 2, 2, 1, 1)
        self.btn_ingestion_ingestor_2d_camera_tuner = QPushButton(self.tab_cfg_ingestion_ingestor)
        self.btn_ingestion_ingestor_2d_camera_tuner.setText("Tuner")
        self.btn_ingestion_ingestor_2d_camera_tuner.setMinimumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_2d_camera_tuner.setMaximumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_2d_camera_tuner.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.btn_ingestion_ingestor_2d_camera_tuner, 2, 3, 1, 1, Qt.AlignHCenter)
        
        # Ingestion pipline: show only when source is 2D camera
        self.lbl_ingestion_ingestor_pipeline = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_pipeline.setText("Ingestion Pipeline")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_pipeline, 3, 1, 1, 1)
        self.le_ingestion_ingestor_pipeline = QLineEdit(self.tab_cfg_ingestion_ingestor)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.le_ingestion_ingestor_pipeline, 3, 2, 1, 1)
        
        # When source is 3D camera
        self.lbl_ingestion_ingestor_3d_camera_type = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_3d_camera_type.setText("Camera Type")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_3d_camera_type, 4, 1, 1, 1)
        self.cbbox_ingestion_ingestor_3d_camera_type = QComboBox(self.tab_cfg_ingestion_ingestor)
        self.cbbox_ingestion_ingestor_3d_camera_type.addItem("Realsense (D435/D435i)")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.cbbox_ingestion_ingestor_3d_camera_type, 4, 2, 1, 1)
        self.btn_ingestion_ingestor_realsense_viewer = QPushButton(self.tab_cfg_ingestion_ingestor)
        self.btn_ingestion_ingestor_realsense_viewer.setText("Viewer")
        self.btn_ingestion_ingestor_realsense_viewer.setMinimumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_realsense_viewer.setMaximumSize(QSize(65, 16777215))
        self.btn_ingestion_ingestor_realsense_viewer.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.btn_ingestion_ingestor_realsense_viewer, 4, 3, 1, 1, Qt.AlignHCenter)
        
        # Serial number: show only when source is GigE/Realsense camera
        self.lbl_ingestion_ingestor_serial_number = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_serial_number.setText("Serial Number")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_serial_number, 5, 1, 1, 1)
        self.le_ingestion_ingestor_serial_number = QLineEdit(self.tab_cfg_ingestion_ingestor)
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.le_ingestion_ingestor_serial_number, 5, 2, 1, 1)
        
        # Frame rate: show only when source is Realsense camera
        self.lbl_ingestion_ingestor_frame_rate = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_frame_rate.setText("Frame Rate")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_frame_rate, 6, 1, 1, 1)
        self.le_ingestion_ingestor_frame_rate = QLineEdit(self.tab_cfg_ingestion_ingestor)
        self.le_ingestion_ingestor_frame_rate.setText("30")
        self.le_ingestion_ingestor_frame_rate.setValidator(QIntValidator())
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.le_ingestion_ingestor_frame_rate, 6, 2, 1, 1)
        
        # Poll interval: show only when source is not 3D camera
        self.lbl_ingestion_ingestor_poll_interval = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_poll_interval.setText("Poll Interval (s)")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_poll_interval, 7, 1, 1, 1)
        self.le_ingestion_ingestor_poll_interval = QLineEdit(self.tab_cfg_ingestion_ingestor)
        self.le_ingestion_ingestor_poll_interval.setText("0.2")
        self.le_ingestion_ingestor_poll_interval.setValidator(
            QDoubleValidator(0.0, 10.0, 6, notation=QDoubleValidator.StandardNotation))
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.le_ingestion_ingestor_poll_interval, 7, 2, 1, 1)
        
        # IMU: show only when source is 3D camera
        self.lbl_ingestion_ingestor_imu = KeyLabel(self.tab_cfg_ingestion_ingestor)
        self.lbl_ingestion_ingestor_imu.setText("IMU")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_imu, 8, 1, 1, 1)
        self.cbbox_ingestion_ingestor_imu = QComboBox(self.tab_cfg_ingestion_ingestor)
        self.cbbox_ingestion_ingestor_imu.addItem("Off")
        self.cbbox_ingestion_ingestor_imu.addItem("On")
        self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.cbbox_ingestion_ingestor_imu, 8, 2, 1, 1)
        
        # SW trigger: show for any source
        # self.lbl_ingestion_ingestor_sw_trigger = KeyLabel(self.tab_cfg_ingestion_ingestor)
        # self.lbl_ingestion_ingestor_sw_trigger.setText("Running on Deploy")
        # self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.lbl_ingestion_ingestor_sw_trigger, 9, 1, 1, 1)
        # self.cbbox_ingestion_ingestor_sw_trigger = QComboBox(self.tab_cfg_ingestion_ingestor)
        # self.cbbox_ingestion_ingestor_sw_trigger.addItem("True")
        # self.cbbox_ingestion_ingestor_sw_trigger.addItem("False")
        # self.grid_lyt_cfg_ingestion_ingestor.addWidget(self.cbbox_ingestion_ingestor_sw_trigger, 9, 2, 1, 1)
        
        spacer_ingestion_ingestor_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_ingestor.addItem(spacer_ingestion_ingestor_left, 10, 0, 1, 1)
        spacer_ingestion_ingestor_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_ingestion_ingestor.addItem(spacer_ingestion_ingestor_bottom, 10, 1, 1, 1)
        spacer_ingestion_ingestor_right = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_ingestor.addItem(spacer_ingestion_ingestor_right, 10, 4, 1, 1)

        # Only show video file options, and hide others
        self._configuration_ingestion_source_is_video_file()

        # --------------------------- Algorithm --------------------------- #
        self.tab_cfg_ingestion_algorithm = QWidget()
        self.grid_lyt_cfg_ingestion_algorithm = QGridLayout(self.tab_cfg_ingestion_algorithm)
        self.tab_wgt_cfg_ingestion.addTab(self.tab_cfg_ingestion_algorithm, "Algorithm")
        
        # UDF: show other items only when the box is checked
        self.ckbox_ingestion_algorithm_udf = QCheckBox(self.tab_cfg_ingestion_algorithm)
        self.ckbox_ingestion_algorithm_udf.setText("UDF")
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.ckbox_ingestion_algorithm_udf, 0, 1, 1, 1)
        self.btn_ingestion_algorithm_import_udf = QPushButton(self.tab_cfg_ingestion_algorithm)
        self.btn_ingestion_algorithm_import_udf.setText("Import")
        self.btn_ingestion_algorithm_import_udf.setMaximumSize(QSize(65, 16777215))
        self.btn_ingestion_algorithm_import_udf.setMinimumSize(QSize(65, 16777215))
        self.btn_ingestion_algorithm_import_udf.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.btn_ingestion_algorithm_import_udf, 0, 2, 1, 1)
        # Button: reset udf
        self.btn_ingestion_algorithm_reset_udf = QPushButton(self.tab_cfg_ingestion_algorithm)
        self.btn_ingestion_algorithm_reset_udf.setText("Reset")
        self.btn_ingestion_algorithm_reset_udf.setMinimumSize(QSize(65, 16777215))
        self.btn_ingestion_algorithm_reset_udf.setMaximumSize(QSize(65, 16777215))
        self.btn_ingestion_algorithm_reset_udf.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.btn_ingestion_algorithm_reset_udf, 0, 3, 1, 1)

        # UDF type
        self.lbl_ingestion_algorithm_udf_type = KeyLabel(self.tab_cfg_ingestion_algorithm)
        self.lbl_ingestion_algorithm_udf_type.setText("Type")
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.lbl_ingestion_algorithm_udf_type, 1, 1, 1, 1)
        self.cbbox_ingestion_algorithm_udf_type = QComboBox(self.tab_cfg_ingestion_algorithm)
        self.cbbox_ingestion_algorithm_udf_type.addItem("Python")
        self.cbbox_ingestion_algorithm_udf_type.addItem("Native")
        self.cbbox_ingestion_algorithm_udf_type.addItem("Raw_native")
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.cbbox_ingestion_algorithm_udf_type, 1, 2, 1, 1)
        
        # UDF name
        self.lbl_ingestion_algorithm_udf_name = KeyLabel(self.tab_cfg_ingestion_algorithm)
        self.lbl_ingestion_algorithm_udf_name.setText("Name")
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.lbl_ingestion_algorithm_udf_name, 2, 1, 1, 1)
        self.le_ingestion_algorithm_udf_name = QLineEdit(self.tab_cfg_ingestion_algorithm)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.le_ingestion_algorithm_udf_name, 2, 2, 1, 1)
        
        # UDF max workers
        self.lbl_ingestion_algorithm_udf_max_workers = KeyLabel(self.tab_cfg_ingestion_algorithm)
        self.lbl_ingestion_algorithm_udf_max_workers.setText("Max Workers")
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.lbl_ingestion_algorithm_udf_max_workers, 3, 1, 1, 1)
        self.le_ingestion_algorithm_udf_max_workers = QLineEdit(self.tab_cfg_ingestion_algorithm)
        self.le_ingestion_algorithm_udf_max_workers.setText("4")
        self.le_ingestion_algorithm_udf_max_workers.setValidator(QIntValidator())
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.le_ingestion_algorithm_udf_max_workers, 3, 2, 1, 1)
        
        self.counter_of_ingestion_udf_added_items = 10
        # del_btn: (lbl_parameter_name, wgt_value_type)
        self.ingestion_udf_added_items = {}
        # parameter_name: del_btn
        # This dict for avoiding the added parameter name is duplicate
        self.ingestion_udf_added_keys = {}
        
        # Button for adding new (key/value) pairs
        self.btn_ingestion_algorithm_udf_add = CircleButton(char="+", parent=self.tab_cfg_ingestion_algorithm)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(self.btn_ingestion_algorithm_udf_add, 100, 1, 1, 1, Qt.AlignHCenter)
        
        spacer_ingestion_algorithm_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_algorithm.addItem(spacer_ingestion_algorithm_left, 101, 0, 1, 1)
        spacer_ingestion_algorithm_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_ingestion_algorithm.addItem(spacer_ingestion_algorithm_bottom, 101, 1, 1, 1)
        spacer_ingestion_algorithm_right = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_algorithm.addItem(spacer_ingestion_algorithm_right, 101, 4, 1, 1)

        # Hide all udf items when the checkbox is unchecked
        self._hide_configuration_ingestion_udf_items()

        # --------------------------- Interface --------------------------- #
        self.tab_cfg_ingestion_interface = QWidget()
        self.grid_lyt_cfg_ingestion_interface = QGridLayout(self.tab_cfg_ingestion_interface)
        self.tab_wgt_cfg_ingestion.addTab(self.tab_cfg_ingestion_interface, "Interface")

        # Server
        self.lbl_ingestion_interface_server = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_server.setText("Server")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_server, 0, 1, 1, 1)
        le_ingestion_interface_ph = QLineEdit(self.tab_cfg_ingestion_interface)
        le_ingestion_interface_ph.setStyleSheet("border: none;")
        le_ingestion_interface_ph.setReadOnly(True)
        self.grid_lyt_cfg_ingestion_interface.addWidget(le_ingestion_interface_ph, 0, 2, 1, 1)
        # NOTE: the button is a placeholder
        btn_ingestion_interface_ph = QPushButton(self.tab_cfg_ingestion_algorithm)
        btn_ingestion_interface_ph.setText("")
        btn_ingestion_interface_ph.setMinimumSize(QSize(65, 16777215))
        btn_ingestion_interface_ph.setMaximumSize(QSize(65, 16777215))
        btn_ingestion_interface_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_ingestion_interface.addWidget(btn_ingestion_interface_ph, 0, 3, 1, 1)
        
        # Server: type
        self.lbl_ingestion_interface_server_type = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_server_type.setText("    Type")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_server_type, 1, 1, 1, 1)
        self.cbbox_ingestion_interface_server_type = QComboBox(self.tab_cfg_ingestion_interface)
        self.cbbox_ingestion_interface_server_type.addItem("IPC")
        self.cbbox_ingestion_interface_server_type.addItem("TCP")
        self.cbbox_ingestion_interface_server_type.setCurrentIndex(1)
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.cbbox_ingestion_interface_server_type, 1, 2, 1, 1)
        
        # Server: endpoint for ipc
        self.lbl_ingestion_interface_server_endpoint_ipc = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_server_endpoint_ipc.setText("    EndPoint")
        self.lbl_ingestion_interface_server_endpoint_ipc.hide()
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_server_endpoint_ipc, 2, 1, 1, 1)
        self.le_ingestion_interface_server_endpoint_ipc = QLineEdit(self.tab_cfg_ingestion_interface)
        self.le_ingestion_interface_server_endpoint_ipc.setText("/EII/sockets")
        self.le_ingestion_interface_server_endpoint_ipc.setReadOnly(True)
        self.le_ingestion_interface_server_endpoint_ipc.setStyleSheet(SS_LE_READ_ONLY)
        self.le_ingestion_interface_server_endpoint_ipc.hide()
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.le_ingestion_interface_server_endpoint_ipc, 2, 2, 1, 1)
        
        # Server: endpoint for tcp
        self.lbl_ingestion_interface_server_endpoint_tcp = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_server_endpoint_tcp.setText("    EndPoint")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_server_endpoint_tcp, 3, 1, 1, 1)
        self.le_ingestion_interface_server_endpoint_tcp = QLineEdit(self.tab_cfg_ingestion_interface)
        vi_address = "0.0.0.0:" + get_random_server_port()
        self.le_ingestion_interface_server_endpoint_tcp.setText(vi_address)
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.le_ingestion_interface_server_endpoint_tcp, 3, 2, 1, 1)
        
        # Publisher
        self.lbl_ingestion_interface_publisher = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_publisher.setText("Publisher")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_publisher, 4, 1, 1, 1)
        le_ingestion_interface_publisher = QLineEdit(self.tab_cfg_ingestion_interface)
        le_ingestion_interface_publisher.setStyleSheet("border: none")
        le_ingestion_interface_publisher.setReadOnly(True)
        self.grid_lyt_cfg_ingestion_interface.addWidget(le_ingestion_interface_publisher, 4, 2, 1, 1)
        
        # Publisher: type
        self.lbl_ingestion_interface_publisher_type = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_publisher_type.setText("    Type")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_publisher_type, 5, 1, 1, 1)
        self.cbbox_ingestion_interface_publisher_type = QComboBox(self.tab_cfg_ingestion_interface)
        self.cbbox_ingestion_interface_publisher_type.addItem("IPC")
        self.cbbox_ingestion_interface_publisher_type.addItem("TCP")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.cbbox_ingestion_interface_publisher_type, 5, 2, 1, 1)
        
        # Publisher: endpoint for ipc
        self.lbl_ingestion_interface_publisher_endpoint_ipc = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_publisher_endpoint_ipc.setText("    EndPoint")
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_publisher_endpoint_ipc, 6, 1, 1, 1)
        self.le_ingestion_interface_publisher_endpoint_ipc = QLineEdit(self.tab_cfg_ingestion_interface)
        self.le_ingestion_interface_publisher_endpoint_ipc.setText("/EII/sockets")
        self.le_ingestion_interface_publisher_endpoint_ipc.setReadOnly(True)
        self.le_ingestion_interface_publisher_endpoint_ipc.setStyleSheet(SS_LE_READ_ONLY)
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.le_ingestion_interface_publisher_endpoint_ipc, 6, 2, 1, 1)
        
        # Publisher: endpoint for tcp
        self.lbl_ingestion_interface_publisher_endpoint_tcp = KeyLabel(self.tab_cfg_ingestion_interface)
        self.lbl_ingestion_interface_publisher_endpoint_tcp.setText("    EndPoint")
        self.lbl_ingestion_interface_publisher_endpoint_tcp.hide()
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.lbl_ingestion_interface_publisher_endpoint_tcp, 7, 1, 1, 1)
        self.le_ingestion_interface_publisher_endpoint_tcp = QLineEdit(self.tab_cfg_ingestion_interface)
        vi_pub_tcp_address = "0.0.0.0:" + get_random_msgbus_port()
        self.le_ingestion_interface_publisher_endpoint_tcp.setText(vi_pub_tcp_address)
        self.le_ingestion_interface_publisher_endpoint_tcp.hide()
        self.grid_lyt_cfg_ingestion_interface.addWidget(self.le_ingestion_interface_publisher_endpoint_tcp, 7, 2, 1, 1)
        
        spacer_ingestion_interface_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_interface.addItem(spacer_ingestion_interface_left, 8, 0, 1, 1)
        spacer_ingestion_interface_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_ingestion_interface.addItem(spacer_ingestion_interface_bottom, 8, 1, 1, 1)
        spacer_ingestion_interface_bottom = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_ingestion_interface.addItem(spacer_ingestion_interface_bottom, 8, 4, 1, 1)

        self.grid_lyt_main_inside.addWidget(self.wgt_cfg_ingestion, 0, 2, 1, 1)

    def init_configuration_analytics(self):
        self.wgt_cfg_analytics = QWidget(self.wgt_main_inside)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.wgt_cfg_analytics.sizePolicy().hasHeightForWidth())
        self.wgt_cfg_analytics.setSizePolicy(sizePolicy)
        self.grid_lyt_cfg_analytics = QGridLayout(self.wgt_cfg_analytics)
        
        self.lbl_cfg_analytics = QLabel(self.wgt_cfg_analytics)
        self.lbl_cfg_analytics.setText("Configuration")
        self.lbl_cfg_analytics.setAlignment(Qt.AlignCenter)
        self.grid_lyt_cfg_analytics.addWidget(self.lbl_cfg_analytics, 0, 0, 1, 1)
        
        self.tab_wgt_cfg_analytics = QTabWidget(self.wgt_cfg_analytics)
        self.grid_lyt_cfg_analytics.addWidget(self.tab_wgt_cfg_analytics, 1, 0, 1, 1)
        
        # --------------------------- Algorithm --------------------------- #
        self.tab_cfg_analytics_algorithm = QWidget()
        self.grid_lyt_cfg_analytics_algorithm = QGridLayout(self.tab_cfg_analytics_algorithm)
        self.tab_wgt_cfg_analytics.addTab(self.tab_cfg_analytics_algorithm, "Algorithm")
        
        # UDF: show other items only when the box is checked
        self.ckbox_analytics_algorithm_udf = QCheckBox(self.tab_cfg_analytics_algorithm)
        self.ckbox_analytics_algorithm_udf.setText("UDF")
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.ckbox_analytics_algorithm_udf, 0, 1, 1, 1)
        self.btn_analytics_algorithm_import_udf = QPushButton(self.tab_cfg_analytics_algorithm)
        self.btn_analytics_algorithm_import_udf.setText("Import")
        self.btn_analytics_algorithm_import_udf.setMaximumSize(QSize(65, 16777215))
        self.btn_analytics_algorithm_import_udf.setMinimumSize(QSize(65, 16777215))
        self.btn_analytics_algorithm_import_udf.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.btn_analytics_algorithm_import_udf, 0, 2, 1, 1)
        # Button: reset udf
        self.btn_analytics_algorithm_reset_udf = QPushButton(self.tab_cfg_analytics_algorithm)
        self.btn_analytics_algorithm_reset_udf.setText("Reset")
        self.btn_analytics_algorithm_reset_udf.setMinimumSize(QSize(65, 16777215))
        self.btn_analytics_algorithm_reset_udf.setMaximumSize(QSize(65, 16777215))
        self.btn_analytics_algorithm_reset_udf.setStyleSheet(SS_MINI_BTN_ENABLE)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.btn_analytics_algorithm_reset_udf, 0, 3, 1, 1)

        # UDF type
        self.lbl_analytics_algorithm_udf_type = KeyLabel(self.tab_cfg_analytics_algorithm)
        self.lbl_analytics_algorithm_udf_type.setText("Type")
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.lbl_analytics_algorithm_udf_type, 1, 1, 1, 1)
        self.cbbox_analytics_algorithm_udf_type = QComboBox(self.tab_cfg_analytics_algorithm)
        self.cbbox_analytics_algorithm_udf_type.addItem("Python")
        self.cbbox_analytics_algorithm_udf_type.addItem("Native")
        self.cbbox_analytics_algorithm_udf_type.addItem("Raw_native")
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.cbbox_analytics_algorithm_udf_type, 1, 2, 1, 1)
        
        # UDF name
        self.lbl_analytics_algorithm_udf_name = KeyLabel(self.tab_cfg_analytics_algorithm)
        self.lbl_analytics_algorithm_udf_name.setText("Name")
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.lbl_analytics_algorithm_udf_name, 2, 1, 1, 1)
        self.le_analytics_algorithm_udf_name = QLineEdit(self.tab_cfg_analytics_algorithm)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.le_analytics_algorithm_udf_name, 2, 2, 1, 1)
        
        # UDF max workers
        self.lbl_analytics_algorithm_udf_max_workers = KeyLabel(self.tab_cfg_analytics_algorithm)
        self.lbl_analytics_algorithm_udf_max_workers.setText("Max Workers")
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.lbl_analytics_algorithm_udf_max_workers, 3, 1, 1, 1)
        self.le_analytics_algorithm_udf_max_workers = QLineEdit(self.tab_cfg_analytics_algorithm)
        self.le_analytics_algorithm_udf_max_workers.setText("4")
        self.le_analytics_algorithm_udf_max_workers.setValidator(QIntValidator())
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.le_analytics_algorithm_udf_max_workers, 3, 2, 1, 1)
        
        self.counter_of_analytics_udf_added_items = 10
        # del_btn: (lbl_parameter_name, wgt_value_type)
        self.analytics_udf_added_items = {} 
        # parameter_name: del_btn
        # This dict for avoiding the added parameter name is duplicate
        self.analytics_udf_added_keys = {}
        
        # Button for adding new (key/value) pairs
        self.btn_analytics_algorithm_udf_add = CircleButton(char="+", parent=self.tab_cfg_analytics_algorithm)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(self.btn_analytics_algorithm_udf_add, 100, 1, 1, 1, Qt.AlignHCenter)
        
        spacer_analytics_algorithm_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_analytics_algorithm.addItem(spacer_analytics_algorithm_left, 101, 0, 1, 1)
        spacer_analytics_algorithm_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_analytics_algorithm.addItem(spacer_analytics_algorithm_bottom, 101, 1, 1, 1)
        spacer_analytics_algorithm_right = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_analytics_algorithm.addItem(spacer_analytics_algorithm_right, 101, 4, 1, 1)

        # Hide all udf items when the checkbox is unchecked
        self._hide_configuration_analytics_udf_items()

        # --------------------------- Interface --------------------------- #
        self.tab_cfg_analytics_interface = QWidget()
        self.grid_lyt_cfg_analytics_interface = QGridLayout(self.tab_cfg_analytics_interface)
        self.tab_wgt_cfg_analytics.addTab(self.tab_cfg_analytics_interface, "Interface")

        # Subscriber
        self.lbl_analytics_interface_subscriber = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_subscriber.setText("Subscriber")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_subscriber, 0, 1, 1, 1)
        le_analytics_interface_ph = QLineEdit(self.tab_cfg_analytics_interface)
        le_analytics_interface_ph.setStyleSheet("border: none;")
        le_analytics_interface_ph.setReadOnly(True)
        self.grid_lyt_cfg_analytics_interface.addWidget(le_analytics_interface_ph, 0, 2, 1, 1)
        # NOTE: the button is a placeholder
        btn_analytics_interface_ph = QPushButton(self.tab_cfg_analytics_algorithm)
        btn_analytics_interface_ph.setText("")
        btn_analytics_interface_ph.setMinimumSize(QSize(65, 16777215))
        btn_analytics_interface_ph.setMaximumSize(QSize(65, 16777215))
        btn_analytics_interface_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_analytics_interface.addWidget(btn_analytics_interface_ph, 0, 3, 1, 1)
        
        # Subscriber: type
        self.lbl_analytics_interface_subscriber_type = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_subscriber_type.setText("    Type")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_subscriber_type, 1, 1, 1, 1)
        self.cbbox_analytics_interface_subscriber_type = QComboBox(self.tab_cfg_analytics_interface)
        self.cbbox_analytics_interface_subscriber_type.addItem("IPC")
        self.cbbox_analytics_interface_subscriber_type.addItem("TCP")
        self.cbbox_analytics_interface_subscriber_type.setStyleSheet(SS_CKBOX_READ_ONLY)
        self.cbbox_analytics_interface_subscriber_type.setEnabled(False)
        self.grid_lyt_cfg_analytics_interface.addWidget(self.cbbox_analytics_interface_subscriber_type, 1, 2, 1, 1)
        
        # Subscriber: endpoint
        self.lbl_analytics_interface_subscriber_endpoint = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_subscriber_endpoint.setText("    EndPoint")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_subscriber_endpoint, 2, 1, 1, 1)
        self.le_analytics_interface_subscriber_endpoint = QLineEdit(self.tab_cfg_analytics_interface)
        self.le_analytics_interface_subscriber_endpoint.setText(self.le_ingestion_interface_publisher_endpoint_ipc.text())
        self.le_analytics_interface_subscriber_endpoint.setReadOnly(True)
        self.le_analytics_interface_subscriber_endpoint.setStyleSheet(SS_LE_READ_ONLY)
        self.grid_lyt_cfg_analytics_interface.addWidget(self.le_analytics_interface_subscriber_endpoint, 2, 2, 1, 1)
        
        # Publisher
        self.lbl_analytics_interface_publisher = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_publisher.setText("Publisher")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_publisher, 3, 1, 1, 1)
        le_analytics_interface_publisher = QLineEdit(self.tab_cfg_analytics_interface)
        le_analytics_interface_publisher.setStyleSheet("border: none")
        le_analytics_interface_publisher.setReadOnly(True)
        self.grid_lyt_cfg_analytics_interface.addWidget(le_analytics_interface_publisher, 3, 2, 1, 1)
        
        # Publisher: type
        self.lbl_analytics_interface_publisher_type = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_publisher_type.setText("    Type")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_publisher_type, 4, 1, 1, 1)
        self.cbbox_analytics_interface_publisher_type = QComboBox(self.tab_cfg_analytics_interface)
        self.cbbox_analytics_interface_publisher_type.addItem("IPC")
        self.cbbox_analytics_interface_publisher_type.addItem("TCP")
        self.cbbox_analytics_interface_publisher_type.setCurrentIndex(1)
        self.grid_lyt_cfg_analytics_interface.addWidget(self.cbbox_analytics_interface_publisher_type, 4, 2, 1, 1)
        
        # Publisher: endpoint for tcp
        self.lbl_analytics_interface_publisher_endpoint_tcp = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_publisher_endpoint_tcp.setText("    EndPoint")
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_publisher_endpoint_tcp, 5, 1, 1, 1)
        self.le_analytics_interface_publisher_endpoint_tcp = QLineEdit(self.tab_cfg_analytics_interface)
        va_pub_tcp_address = "0.0.0.0:" + get_random_msgbus_port()
        self.le_analytics_interface_publisher_endpoint_tcp.setText(va_pub_tcp_address)
        self.grid_lyt_cfg_analytics_interface.addWidget(self.le_analytics_interface_publisher_endpoint_tcp, 5, 2, 1, 1)

        # Publisher: endpoint for ipc
        self.lbl_analytics_interface_publisher_endpoint_ipc = KeyLabel(self.tab_cfg_analytics_interface)
        self.lbl_analytics_interface_publisher_endpoint_ipc.setText("    EndPoint")
        self.lbl_analytics_interface_publisher_endpoint_ipc.hide()
        self.grid_lyt_cfg_analytics_interface.addWidget(self.lbl_analytics_interface_publisher_endpoint_ipc, 6, 1, 1, 1)
        self.le_analytics_interface_publisher_endpoint_ipc = QLineEdit(self.tab_cfg_analytics_interface)
        self.le_analytics_interface_publisher_endpoint_ipc.setText("/EII/sockets")
        self.le_analytics_interface_publisher_endpoint_ipc.setReadOnly(True)
        self.le_analytics_interface_publisher_endpoint_ipc.setStyleSheet(SS_LE_READ_ONLY)
        self.le_analytics_interface_publisher_endpoint_ipc.hide()
        self.grid_lyt_cfg_analytics_interface.addWidget(self.le_analytics_interface_publisher_endpoint_ipc, 6, 2, 1, 1)
        
        spacer_analytics_interface_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_analytics_interface.addItem(spacer_analytics_interface_left, 7, 0, 1, 1)
        spacer_analytics_interface_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_analytics_interface.addItem(spacer_analytics_interface_bottom, 7, 1, 1, 1)
        spacer_analytics_interface_bottom = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_analytics_interface.addItem(spacer_analytics_interface_bottom, 7, 4, 1, 1)

        self.grid_lyt_main_inside.addWidget(self.wgt_cfg_analytics, 0, 2, 1, 1)

    def init_configuration_output_modules(self):
        self.wgt_cfg_output_modules = QWidget(self.wgt_main_inside)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.wgt_cfg_output_modules.sizePolicy().hasHeightForWidth())
        self.wgt_cfg_output_modules.setSizePolicy(sizePolicy)
        self.grid_lyt_cfg_imagestore = QGridLayout(self.wgt_cfg_output_modules)
        
        self.lbl_cfg_imagestore = QLabel(self.wgt_cfg_output_modules)
        self.lbl_cfg_imagestore.setText("Configuration")
        self.lbl_cfg_imagestore.setAlignment(Qt.AlignCenter)
        self.grid_lyt_cfg_imagestore.addWidget(self.lbl_cfg_imagestore, 0, 0, 1, 1)
        
        self.tab_wgt_cfg_output_modules = QTabWidget(self.wgt_cfg_output_modules)
        self.grid_lyt_cfg_imagestore.addWidget(self.tab_wgt_cfg_output_modules, 1, 0, 1, 1)

        # --------------------------- Image Store --------------------------- #
        self.tab_cfg_imagestore_minio = QWidget()
        self.grid_lyt_cfg_imagestore_minio = QGridLayout(self.tab_cfg_imagestore_minio)
        self.tab_wgt_cfg_output_modules.addTab(self.tab_cfg_imagestore_minio, "Image Store")

        # Enable: show other items only when the box is checked
        self.ckbox_enable_image_store = QCheckBox(self.tab_cfg_imagestore_minio)
        self.ckbox_enable_image_store.setText("Enable")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.ckbox_enable_image_store, 0, 1, 1, 1)

        # Storage Policy
        self.lbl_imagestore_storage_policy = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_storage_policy.setText("---- Storage Policy ----")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_storage_policy, 1, 1, 1, 1)

        # Retention time
        self.lbl_imagestore_minio_retention_time = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_minio_retention_time.setText("    Retention Time (h)")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_minio_retention_time, 2, 1, 1, 1)
        self.le_imagestore_minio_retention_time = QLineEdit(self.tab_cfg_imagestore_minio)
        self.le_imagestore_minio_retention_time.setText("1")
        self.le_imagestore_minio_retention_time.setValidator(QDoubleValidator())
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.le_imagestore_minio_retention_time, 2, 2, 1, 1)
        # NOTE: the button is a placeholder
        btn_imagestore_minio_ph = QPushButton(self.tab_cfg_imagestore_minio)
        btn_imagestore_minio_ph.setText("")
        btn_imagestore_minio_ph.setMinimumSize(QSize(65, 16777215))
        btn_imagestore_minio_ph.setMaximumSize(QSize(65, 16777215))
        btn_imagestore_minio_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_imagestore_minio.addWidget(btn_imagestore_minio_ph, 0, 3, 1, 1)
        
        # Retention poll interval
        self.lbl_imagestore_minio_retention_poll_interval = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_minio_retention_poll_interval.setText("    Check Interval (s)")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_minio_retention_poll_interval, 3, 1, 1, 1)
        self.le_imagestore_minio_retention_poll_interval = QLineEdit(self.tab_cfg_imagestore_minio)
        self.le_imagestore_minio_retention_poll_interval.setText("60")
        self.le_imagestore_minio_retention_poll_interval.setValidator(QDoubleValidator())
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.le_imagestore_minio_retention_poll_interval, 3, 2, 1, 1)
        
        # Storage Policy
        self.lbl_imagestore_interface = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface.setText("---- Interface ----")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface, 4, 1, 1, 1)

        # Subscriber
        self.lbl_imagestore_interface_subscriber = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_subscriber.setText("Subscriber")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_subscriber, 5, 1, 1, 1)
        le_imagestore_interface_subscriber = QLineEdit(self.tab_cfg_imagestore_minio)
        le_imagestore_interface_subscriber.setStyleSheet("border: none")
        le_imagestore_interface_subscriber.setReadOnly(True)
        self.grid_lyt_cfg_imagestore_minio.addWidget(le_imagestore_interface_subscriber, 5, 2, 1, 1)
        # NOTE: the button is a placeholder
        btn_imagestore_interface_ph = QPushButton(self.tab_cfg_imagestore_minio)
        btn_imagestore_interface_ph.setText("")
        btn_imagestore_interface_ph.setMinimumSize(QSize(65, 16777215))
        btn_imagestore_interface_ph.setMaximumSize(QSize(65, 16777215))
        btn_imagestore_interface_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_imagestore_minio.addWidget(btn_imagestore_interface_ph, 5, 3, 1, 1)
        
        # Subscriber: type
        self.lbl_imagestore_interface_subscriber_type = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_subscriber_type.setText("    Type")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_subscriber_type, 6, 1, 1, 1)
        self.cbbox_imagestore_interface_subscriber_type = QComboBox(self.tab_cfg_imagestore_minio)
        self.cbbox_imagestore_interface_subscriber_type.addItem("IPC")
        self.cbbox_imagestore_interface_subscriber_type.addItem("TCP")
        self.cbbox_imagestore_interface_subscriber_type.setCurrentIndex(1)
        self.cbbox_imagestore_interface_subscriber_type.setStyleSheet(SS_CKBOX_READ_ONLY)
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.cbbox_imagestore_interface_subscriber_type, 6, 2, 1, 1)
        
        # Subscriber: endpoint
        self.lbl_imagestore_interface_subscriber_endpoint = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_subscriber_endpoint.setText("    EndPoint")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_subscriber_endpoint, 7, 1, 1, 1)
        self.le_imagestore_interface_subscriber_endpoint = QLineEdit(self.tab_cfg_imagestore_minio)
        self.le_imagestore_interface_subscriber_endpoint.setText(self.le_analytics_interface_publisher_endpoint_tcp.text())
        self.le_imagestore_interface_subscriber_endpoint.setReadOnly(True)
        self.le_imagestore_interface_subscriber_endpoint.setStyleSheet(SS_LE_READ_ONLY)
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.le_imagestore_interface_subscriber_endpoint, 7, 2, 1, 1)
        
        # Server
        self.lbl_imagestore_interface_server = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_server.setText("Server")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_server, 8, 1, 1, 1)
        le_imagestore_interface_ph = QLineEdit(self.tab_cfg_imagestore_minio)
        le_imagestore_interface_ph.setStyleSheet("border: none;")
        le_imagestore_interface_ph.setReadOnly(True)
        self.grid_lyt_cfg_imagestore_minio.addWidget(le_imagestore_interface_ph, 8, 2, 1, 1)
        
        # Server: type
        self.lbl_imagestore_interface_server_type = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_server_type.setText("    Type")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_server_type, 9, 1, 1, 1)
        self.cbbox_imagestore_interface_server_type = QComboBox(self.tab_cfg_imagestore_minio)
        self.cbbox_imagestore_interface_server_type.addItem("IPC")
        self.cbbox_imagestore_interface_server_type.addItem("TCP")
        self.cbbox_imagestore_interface_server_type.setCurrentIndex(1)
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.cbbox_imagestore_interface_server_type, 9, 2, 1, 1)
        
        # Server: endpoint for tcp
        self.lbl_imagestore_interface_server_endpoint_tcp = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_server_endpoint_tcp.setText("    EndPoint")
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_server_endpoint_tcp, 10, 1, 1, 1)
        self.le_imagestore_interface_server_endpoint_tcp = QLineEdit(self.tab_cfg_imagestore_minio)
        image_store_server_port = "0.0.0.0:" + get_random_server_port()
        self.le_imagestore_interface_server_endpoint_tcp.setText(image_store_server_port)
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.le_imagestore_interface_server_endpoint_tcp, 10, 2, 1, 1)

        # Server: endpoint for ipc
        self.lbl_imagestore_interface_server_endpoint_ipc = KeyLabel(self.tab_cfg_imagestore_minio)
        self.lbl_imagestore_interface_server_endpoint_ipc.setText("    EndPoint")
        self.lbl_imagestore_interface_server_endpoint_ipc.hide()
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.lbl_imagestore_interface_server_endpoint_ipc, 11, 1, 1, 1)
        self.le_imagestore_interface_server_endpoint_ipc = QLineEdit(self.tab_cfg_imagestore_minio)
        self.le_imagestore_interface_server_endpoint_ipc.setText("/EII/sockets")
        self.le_imagestore_interface_server_endpoint_ipc.setReadOnly(True)
        self.le_imagestore_interface_server_endpoint_ipc.setStyleSheet(SS_LE_READ_ONLY)
        self.le_imagestore_interface_server_endpoint_ipc.hide()
        self.grid_lyt_cfg_imagestore_minio.addWidget(self.le_imagestore_interface_server_endpoint_ipc, 11, 2, 1, 1)

        spacer_imagestore_minio_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_imagestore_minio.addItem(spacer_imagestore_minio_left, 12, 0, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_imagestore_minio.addItem(spacer_imagestore_minio_bottom, 12, 1, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_imagestore_minio.addItem(spacer_imagestore_minio_bottom, 12, 4, 1, 1)

        # Hide all items when the 'Enable' checkbox is unchecked
        self._hide_configuration_image_store_items()
        
        # --------------------------- OPC UA --------------------------- #
        self.tab_cfg_opcua = QWidget()
        self.grid_lyt_cfg_opcua = QGridLayout(self.tab_cfg_opcua)
        self.tab_wgt_cfg_output_modules.addTab(self.tab_cfg_opcua, "OPC UA")

        # Enable: show other items only when the box is checked
        self.ckbox_enable_opcua = QCheckBox(self.tab_cfg_opcua)
        self.ckbox_enable_opcua.setText("Enable")
        self.grid_lyt_cfg_opcua.addWidget(self.ckbox_enable_opcua, 0, 1, 1, 1)
        # NOTE: the button is a placeholder
        btn_opcua_ph = QPushButton(self.tab_cfg_opcua)
        btn_opcua_ph.setText("")
        btn_opcua_ph.setMinimumSize(QSize(65, 16777215))
        btn_opcua_ph.setMaximumSize(QSize(65, 16777215))
        btn_opcua_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_opcua.addWidget(btn_opcua_ph, 0, 3, 1, 1)

        # OPCUA Config
        self.lbl_opcua_config = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_config.setText("---- OPCUA Config ----")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_config, 1, 1, 1, 1)

        # Topics
        self.lbl_opcua_topics = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_topics.setText("    Topics")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_topics, 2, 1, 1, 1)
        self.le_opcua_topics = QLineEdit(self.tab_cfg_opcua)
        self.le_opcua_topics.setText("opcua_cam_serial1_results")
        self.grid_lyt_cfg_opcua.addWidget(self.le_opcua_topics, 2, 2, 1, 1)
        
        # Address
        self.lbl_opcua_address = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_address.setText("    Address")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_address, 3, 1, 1, 1)
        self.le_opcua_address = QLineEdit(self.tab_cfg_opcua)
        opcua_cfg_address = "opcua,0.0.0.0:" + get_random_server_port()
        self.le_opcua_address.setText(opcua_cfg_address)
        self.grid_lyt_cfg_opcua.addWidget(self.le_opcua_address, 3, 2, 1, 1)
        
        # Interface
        self.lbl_opcua_interface = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_interface.setText("---- Interface ----")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_interface, 4, 1, 1, 1)

        # Subscriber
        self.lbl_opcua_interface_subscriber = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_interface_subscriber.setText("Subscriber")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_interface_subscriber, 5, 1, 1, 1)
                
        # Subscriber: type
        self.lbl_opcua_interface_subscriber_type = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_interface_subscriber_type.setText("    Type")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_interface_subscriber_type, 6, 1, 1, 1)
        self.cbbox_opcua_interface_subscriber_type = QComboBox(self.tab_cfg_opcua)
        self.cbbox_opcua_interface_subscriber_type.addItem("IPC")
        self.cbbox_opcua_interface_subscriber_type.addItem("TCP")
        self.cbbox_opcua_interface_subscriber_type.setCurrentIndex(1)
        self.cbbox_opcua_interface_subscriber_type.setStyleSheet(SS_CKBOX_READ_ONLY)
        self.grid_lyt_cfg_opcua.addWidget(self.cbbox_opcua_interface_subscriber_type, 6, 2, 1, 1)
        
        # Subscriber: endpoint
        self.lbl_opcua_interface_subscriber_endpoint = KeyLabel(self.tab_cfg_opcua)
        self.lbl_opcua_interface_subscriber_endpoint.setText("    EndPoint")
        self.grid_lyt_cfg_opcua.addWidget(self.lbl_opcua_interface_subscriber_endpoint, 7, 1, 1, 1)
        self.le_opcua_interface_subscriber_endpoint = QLineEdit(self.tab_cfg_opcua)
        self.le_opcua_interface_subscriber_endpoint.setText(self.le_analytics_interface_publisher_endpoint_tcp.text())
        self.le_opcua_interface_subscriber_endpoint.setReadOnly(True)
        self.le_opcua_interface_subscriber_endpoint.setStyleSheet(SS_LE_READ_ONLY)
        self.grid_lyt_cfg_opcua.addWidget(self.le_opcua_interface_subscriber_endpoint, 7, 2, 1, 1)

        spacer_imagestore_minio_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_opcua.addItem(spacer_imagestore_minio_left, 12, 0, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_opcua.addItem(spacer_imagestore_minio_bottom, 12, 1, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_opcua.addItem(spacer_imagestore_minio_bottom, 12, 4, 1, 1)

        # Hide all items when the 'Enable' checkbox is unchecked
        self._hide_configuration_opcua_items()

        # --------------------------- TCP/IP --------------------------- #
        self.tab_cfg_tcp = QWidget()
        self.grid_lyt_cfg_tcp = QGridLayout(self.tab_cfg_tcp)
        self.tab_wgt_cfg_output_modules.addTab(self.tab_cfg_tcp, "TCP/IP")

        # Enable: show other items only when the box is checked
        self.ckbox_enable_tcp = QCheckBox(self.tab_cfg_tcp)
        self.ckbox_enable_tcp.setText("Enable")
        self.grid_lyt_cfg_tcp.addWidget(self.ckbox_enable_tcp, 0, 1, 1, 1)
        # NOTE: the button is a placeholder
        btn_tcp_ph = QPushButton(self.tab_cfg_tcp)
        btn_tcp_ph.setText("")
        btn_tcp_ph.setMinimumSize(QSize(65, 16777215))
        btn_tcp_ph.setMaximumSize(QSize(65, 16777215))
        btn_tcp_ph.setStyleSheet("border: none;")
        self.grid_lyt_cfg_tcp.addWidget(btn_tcp_ph, 0, 3, 1, 1)

        # TCP Config
        self.lbl_tcp_config = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_config.setText("---- TCP Config ----")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_config, 1, 1, 1, 1)

        # IP
        self.lbl_tcp_ip = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_ip.setText("    Server IP")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_ip, 2, 1, 1, 1)
        self.le_tcp_ip = QLineEdit(self.tab_cfg_tcp)
        self.le_tcp_ip.setText("127.0.0.1")
        self.grid_lyt_cfg_tcp.addWidget(self.le_tcp_ip, 2, 2, 1, 1)
        
        # Port
        self.lbl_tcp_port = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_port.setText("    Server Port")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_port, 3, 1, 1, 1)
        self.le_tcp_port = QLineEdit(self.tab_cfg_tcp)
        tcp_output_port = get_random_tcp_output_port()
        self.le_tcp_port.setText(tcp_output_port)
        self.grid_lyt_cfg_tcp.addWidget(self.le_tcp_port, 3, 2, 1, 1)
        
        # Interface
        self.lbl_tcp_interface = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_interface.setText("---- Interface ----")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_interface, 4, 1, 1, 1)

        # Subscriber
        self.lbl_tcp_interface_subscriber = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_interface_subscriber.setText("Subscriber")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_interface_subscriber, 5, 1, 1, 1)
                
        # Subscriber: type
        self.lbl_tcp_interface_subscriber_type = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_interface_subscriber_type.setText("    Type")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_interface_subscriber_type, 6, 1, 1, 1)
        self.cbbox_tcp_interface_subscriber_type = QComboBox(self.tab_cfg_tcp)
        self.cbbox_tcp_interface_subscriber_type.addItem("IPC")
        self.cbbox_tcp_interface_subscriber_type.addItem("TCP")
        self.cbbox_tcp_interface_subscriber_type.setCurrentIndex(1)
        self.cbbox_tcp_interface_subscriber_type.setStyleSheet(SS_CKBOX_READ_ONLY)
        self.grid_lyt_cfg_tcp.addWidget(self.cbbox_tcp_interface_subscriber_type, 6, 2, 1, 1)
        
        # Subscriber: endpoint
        self.lbl_tcp_interface_subscriber_endpoint = KeyLabel(self.tab_cfg_tcp)
        self.lbl_tcp_interface_subscriber_endpoint.setText("    EndPoint")
        self.grid_lyt_cfg_tcp.addWidget(self.lbl_tcp_interface_subscriber_endpoint, 7, 1, 1, 1)
        self.le_tcp_interface_subscriber_endpoint = QLineEdit(self.tab_cfg_tcp)
        self.le_tcp_interface_subscriber_endpoint.setText(self.le_analytics_interface_publisher_endpoint_tcp.text())
        self.le_tcp_interface_subscriber_endpoint.setReadOnly(True)
        self.le_tcp_interface_subscriber_endpoint.setStyleSheet(SS_LE_READ_ONLY)
        self.grid_lyt_cfg_tcp.addWidget(self.le_tcp_interface_subscriber_endpoint, 7, 2, 1, 1)

        spacer_imagestore_minio_left = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_tcp.addItem(spacer_imagestore_minio_left, 12, 0, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.grid_lyt_cfg_tcp.addItem(spacer_imagestore_minio_bottom, 12, 1, 1, 1)
        spacer_imagestore_minio_bottom = QSpacerItem(20, 40, QSizePolicy.Fixed, QSizePolicy.Minimum)
        self.grid_lyt_cfg_tcp.addItem(spacer_imagestore_minio_bottom, 12, 4, 1, 1)

        # Hide all items when the 'Enable' checkbox is unchecked
        self._hide_configuration_tcp_items()

        self.grid_lyt_main_inside.addWidget(self.wgt_cfg_output_modules, 0, 2, 1, 1)

    def apply_existing_config(self):
        print("Applying existing config...")
        # Check new project or exist project
        self.flag_project = "new" # Enum: "new"/"exist"
        self.comp_list = []
        if not os.path.exists(self.project_path) or not os.listdir(self.project_path):
            print("It's new project")
            # New project has an empty folder
            self.flag_project = "new"
        else:
            print("It's exsiting project")
            self.flag_project = "exist"
            # Check mode "vi_va" or "only_vi"
            self.comp_list = os.listdir(self.project_path)
            if "VideoAnalytics" in self.comp_list:
                self.vi_va_mode = "vi_va"
            else:
                self.vi_va_mode = "only_vi"

        # Update switch mode button
        if self.flag_project == "new" or self.vi_va_mode == "vi_va":
            flag_mode = "vi_va"
        else:
            flag_mode = "only_vi"
        self._switch_mode_apply_config(flag_mode)

        # Update existing config
        if self.flag_project == "exist":
            self._components_apply_config()
    
    def _switch_mode_apply_config(self, flag_mode):
        if flag_mode == "vi_va":
            self.click_wgt_analytics.show()
            self.line_ingestion_analytics.show()
            self.btn_vi_va.setStyleSheet(SS_STD_BTN_ENABLE)
            self.btn_only_vi.setStyleSheet(SS_STD_BTN_DISABLE)
            self.vi_va_mode = "vi_va"
        else:
            self.click_wgt_analytics.hide()
            self.line_ingestion_analytics.hide()
            self.btn_vi_va.setStyleSheet(SS_STD_BTN_DISABLE)
            self.btn_only_vi.setStyleSheet(SS_STD_BTN_ENABLE)
            self.vi_va_mode = "only_vi"

        # Activate ingestion configuration
        self.activate_module_configuration(self.click_wgt_ingestion)

    def _components_apply_config(self):
        if not self.comp_list or not os.path.exists(self.project_path):
            print("No comp config to apply")
            return

        # Apply VI config
        if EII_MODULE.VI.value in self.comp_list:
            vi_config_file = os.path.join(self.project_path, EII_MODULE.VI.value, EII_FILE_NAME.CONFIG_JSON.value)
            # If config.json was found, load it
            if os.path.exists(vi_config_file):
                with open(vi_config_file, "r") as f:
                    vi_config_dict = json.loads(f.read())
                self._vi_apply_config(vi_config_dict)
        # Apply VA config
        if EII_MODULE.VA.value in self.comp_list:
            va_config_file = os.path.join(self.project_path, EII_MODULE.VA.value, EII_FILE_NAME.CONFIG_JSON.value)
            # If config.json was found, load it
            if os.path.exists(va_config_file):
                with open(va_config_file, "r") as f:
                    va_config_dict = json.loads(f.read())
                self._va_apply_config(va_config_dict)
        # Apply ImageStore config
        if EII_MODULE.ImageStore.value in self.comp_list:
            is_config_file = os.path.join(self.project_path, EII_MODULE.ImageStore.value, EII_FILE_NAME.CONFIG_JSON.value)
            # If config.json was found, load it
            if os.path.exists(is_config_file):
                with open(is_config_file, "r") as f:
                    is_config_dict = json.loads(f.read())
                self._image_store_apply_config(is_config_dict)
        else:
            # need re-init
            if self.vi_va_mode == "only_vi":
                # Link to VI
                interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
                self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
            else:
                # Link to VA
                interface_type = self.cbbox_analytics_interface_publisher_type.currentText()
                self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_analytics_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
                self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
        # Apply opcua config
        if EII_MODULE.OPCUA.value in self.comp_list:
            config_file = os.path.join(self.project_path, EII_MODULE.OPCUA.value, EII_FILE_NAME.CONFIG_JSON.value)
            if os.path.exists(config_file):
                with open(config_file, "r") as f:
                    config_dict = json.loads(f.read())
                self._opcua_apply_config(config_dict)
        else:
            # need re-init
            if self.vi_va_mode == "only_vi":
                # Link to VI
                interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
                self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
            else:
                # Link to VA
                interface_type = self.cbbox_analytics_interface_publisher_type.currentText()
                self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_analytics_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
                self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
        # Apply Tcp config
        if EII_MODULE.TCP.value in self.comp_list:
            config_file = os.path.join(self.project_path, EII_MODULE.TCP.value, EII_FILE_NAME.CONFIG_JSON.value)
            if os.path.exists(config_file):
                with open(config_file, "r") as f:
                    config_dict = json.loads(f.read())
                self._tcp_apply_config(config_dict)
        else:
            # need re-init
            if self.vi_va_mode == "only_vi":
                # Link to VI
                interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
                self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_tcp_interface_subscriber_endpoint.setText(endpoint)
            else:
                # Link to VA
                interface_type = self.cbbox_analytics_interface_publisher_type.currentText()
                self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_analytics_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
                self.le_tcp_interface_subscriber_endpoint.setText(endpoint)

    # Apply existing config to vi ui
    def _vi_apply_config(self, config_dict):
        print("Applying VI config...")
        # Ingestor
        ingestor_info = config_dict["config"]["ingestor"]
        ing_type = ingestor_info["type"]
        if ing_type:
            if ing_type == "opencv":
                self.cbbox_ingestion_ingestor_source.setCurrentText("Video File")
                self.slot_configuration_ingestion_source_changed("Video File")
                self.ckbox_ingestion_ingestor_loop.setChecked(bool(ingestor_info["loop_video"]))
                self.le_ingestion_ingestor_video_file.setText(str(ingestor_info["pipeline"]))
                self.le_ingestion_ingestor_poll_interval.setText(str(ingestor_info["poll_interval"]))
            elif ing_type == "gstreamer":
                self.cbbox_ingestion_ingestor_source.setCurrentText("2D Camera")
                self.slot_configuration_ingestion_source_changed("2D Camera")
                gst_pipe = ingestor_info["pipeline"]
                if "gencamsrc" in gst_pipe:
                    self.cbbox_ingestion_ingestor_2d_camera_type.setCurrentText("GigE")
                elif "v4l2src" in gst_pipe:
                    self.cbbox_ingestion_ingestor_2d_camera_type.setCurrentText("USB")
                elif "rtspsrc" in gst_pipe:
                    self.cbbox_ingestion_ingestor_2d_camera_type.setCurrentText("RTSP")
                else:
                    print("Unknown camera type in vi config file")
                self.le_ingestion_ingestor_pipeline.setText(gst_pipe)
                self.le_ingestion_ingestor_poll_interval.setText(str(ingestor_info["poll_interval"]))
            elif ing_type == "realsense":
                self.cbbox_ingestion_ingestor_source.setCurrentText("3D Camera")
                self.slot_configuration_ingestion_source_changed("3D Camera")
                if "serial" in ingestor_info:
                    self.le_ingestion_ingestor_serial_number.setText(str(ingestor_info["serial"]))
                self.le_ingestion_ingestor_frame_rate.setText(str(ingestor_info["framerate"]))
                if ingestor_info["imu_on"]:
                    self.cbbox_ingestion_ingestor_imu.setCurrentText("On")
                else:
                    self.cbbox_ingestion_ingestor_imu.setCurrentText("Off")
            else:
                print("Unknown ingestor type when apply vi config")
                return
        
        # Algorithm (UDF)
        udf_list = config_dict["config"]["udfs"]
        if udf_list:
            udf_info = udf_list[0]
            self.ckbox_ingestion_algorithm_udf.setChecked(True)
            self._show_configuration_ingestion_udf_items()
            self._configuration_ingestion_udf_load_from_dict(udf_info)
            # Max Workers
            max_workers = config_dict["config"]["max_workers"]
            self.le_ingestion_algorithm_udf_max_workers.setText(str(max_workers))

        # Interface
        interface_info = config_dict["interfaces"]
        server_info = interface_info["Servers"][0]
        if server_info["Type"] == "zmq_tcp":
            self.cbbox_ingestion_interface_server_type.setCurrentText("TCP")
            self.le_ingestion_interface_server_endpoint_tcp.setText(server_info["EndPoint"])
            self.le_ingestion_interface_server_endpoint_tcp.show()
            self.lbl_ingestion_interface_server_endpoint_tcp.show()
            self.le_ingestion_interface_server_endpoint_ipc.hide()
            self.lbl_ingestion_interface_server_endpoint_ipc.hide()
        else:
            self.cbbox_ingestion_interface_server_type.setCurrentText("IPC")
            self.le_ingestion_interface_server_endpoint_ipc.setText(server_info["EndPoint"])
            self.le_ingestion_interface_server_endpoint_ipc.show()
            self.lbl_ingestion_interface_server_endpoint_ipc.show()
            self.le_ingestion_interface_server_endpoint_tcp.hide()
            self.lbl_ingestion_interface_server_endpoint_tcp.hide()

        pub_info = interface_info["Publishers"][0]
        if pub_info["Type"] == "zmq_tcp":
            self.cbbox_ingestion_interface_publisher_type.setCurrentText("TCP")
            self.le_ingestion_interface_publisher_endpoint_tcp.setText(pub_info["EndPoint"])
            self.le_ingestion_interface_publisher_endpoint_tcp.show()
            self.lbl_ingestion_interface_publisher_endpoint_tcp.show()
            self.le_ingestion_interface_publisher_endpoint_ipc.hide()
            self.lbl_ingestion_interface_publisher_endpoint_ipc.hide()
        else:
            self.cbbox_ingestion_interface_publisher_type.setCurrentText("IPC")
            self.le_ingestion_interface_publisher_endpoint_ipc.setText(pub_info["EndPoint"])
            self.le_ingestion_interface_publisher_endpoint_ipc.show()
            self.lbl_ingestion_interface_publisher_endpoint_ipc.show()
            self.le_ingestion_interface_publisher_endpoint_tcp.hide()
            self.lbl_ingestion_interface_publisher_endpoint_tcp.hide()
        
    # Apply existing config to va ui
    def _va_apply_config(self, config_dict):
        print("Applying VA config...")
        # Algorithm (UDF)
        udf_list = config_dict["config"]["udfs"]
        if udf_list:
            udf_info = udf_list[0]
            self.ckbox_analytics_algorithm_udf.setChecked(True)
            self._show_configuration_analytics_udf_items()
            self._configuration_analytics_udf_load_from_dict(udf_info)
            # Max Workers
            max_workers = config_dict["config"]["max_workers"]
            self.le_analytics_algorithm_udf_max_workers.setText(str(max_workers))

        # Interface
        interface_info = config_dict["interfaces"]
        sub_info = interface_info["Subscribers"][0]
        if sub_info["Type"] == "zmq_tcp":
            self.cbbox_analytics_interface_subscriber_type.setCurrentText("TCP")
        else:
            self.cbbox_analytics_interface_subscriber_type.setCurrentText("IPC")
        self.le_analytics_interface_subscriber_endpoint.setText(sub_info["EndPoint"])

        pub_info = interface_info["Publishers"][0]
        if pub_info["Type"] == "zmq_tcp":
            self.cbbox_analytics_interface_publisher_type.setCurrentText("TCP")
            self.le_analytics_interface_publisher_endpoint_tcp.setText(pub_info["EndPoint"])
            self.le_analytics_interface_publisher_endpoint_tcp.show()
            self.lbl_analytics_interface_publisher_endpoint_tcp.show()
            self.le_analytics_interface_publisher_endpoint_ipc.hide()
            self.lbl_analytics_interface_publisher_endpoint_ipc.hide()
        else:
            self.cbbox_analytics_interface_publisher_type.setCurrentText("IPC")
            self.le_analytics_interface_publisher_endpoint_ipc.setText(pub_info["EndPoint"])
            self.le_analytics_interface_publisher_endpoint_ipc.show()
            self.lbl_analytics_interface_publisher_endpoint_ipc.show()
            self.le_analytics_interface_publisher_endpoint_tcp.hide()
            self.lbl_analytics_interface_publisher_endpoint_tcp.hide()

    # Apply existing config to image store ui
    def _image_store_apply_config(self, config_dict):
        print("Applying Image Store config...")
        # Checkbox in layout
        self.ckbox_enable_image_store.setChecked(True)
        self._show_configuration_image_store_items()

        # Minio
        minio_info = config_dict["config"]["minio"]
        if minio_info:
            ret_time_value = minio_info["retentionTime"].split(".")[0]
            self.le_imagestore_minio_retention_time.setText(ret_time_value)
            ret_interval_value = minio_info["retentionPollInterval"].split(".")[0]
            self.le_imagestore_minio_retention_poll_interval.setText(ret_interval_value)
            
        # Interface
        interface_info = config_dict["interfaces"]
        sub_info = interface_info["Subscribers"][0]
        if sub_info["Type"] == "zmq_tcp":
            self.cbbox_imagestore_interface_subscriber_type.setCurrentText("TCP")
        else:
            self.cbbox_imagestore_interface_subscriber_type.setCurrentText("IPC")
        self.le_imagestore_interface_subscriber_endpoint.setText(sub_info["EndPoint"])

        server_info = interface_info["Servers"][0]
        if server_info["Type"] == "zmq_tcp":
            self.cbbox_imagestore_interface_server_type.setCurrentText("TCP")
            self.le_imagestore_interface_server_endpoint_tcp.setText(server_info["EndPoint"])
            self.le_imagestore_interface_server_endpoint_tcp.show()
            self.lbl_imagestore_interface_server_endpoint_tcp.show()
            self.le_imagestore_interface_server_endpoint_ipc.hide()
            self.lbl_imagestore_interface_server_endpoint_ipc.hide()
        else:
            self.cbbox_imagestore_interface_server_type.setCurrentText("IPC")
            self.le_imagestore_interface_server_endpoint_ipc.setText(server_info["EndPoint"])
            self.le_imagestore_interface_server_endpoint_ipc.show()
            self.lbl_imagestore_interface_server_endpoint_ipc.show()
            self.le_imagestore_interface_server_endpoint_tcp.hide()
            self.lbl_imagestore_interface_server_endpoint_tcp.hide()

    # Apply existing config to opcua ui
    def _opcua_apply_config(self, config_dict):
        print("Applying OPCUA config...")
        # Checkbox in layout
        self.ckbox_enable_opcua.setChecked(True)
        self._show_configuration_opcua_items()

        # OPCUA Config
        topics = config_dict["config"]["OpcuaDatabusTopics"]
        if topics:
            topics_str = ",".join(topics)
            self.le_opcua_topics.setText(topics_str)
        address = config_dict["config"]["OpcuaExportCfg"]
        if address:
            self.le_opcua_address.setText(address)
            
        # Interface
        interface_info = config_dict["interfaces"]
        sub_info = interface_info["Subscribers"][0]
        if sub_info["Type"] == "zmq_tcp":
            self.cbbox_opcua_interface_subscriber_type.setCurrentText("TCP")
        else:
            self.cbbox_opcua_interface_subscriber_type.setCurrentText("IPC")
        self.le_opcua_interface_subscriber_endpoint.setText(sub_info["EndPoint"])

    # Apply existing config to tcp ui
    def _tcp_apply_config(self, config_dict):
        print("Applying TCP config...")
        # Checkbox in layout
        self.ckbox_enable_tcp.setChecked(True)
        self._show_configuration_tcp_items()

        # TCP Config
        ip = config_dict["config"]["tcp_server_ip"]
        if ip:
            self.le_tcp_ip.setText(ip)
        port = config_dict["config"]["tcp_server_port"]
        if port:
            self.le_tcp_port.setText(str(port))
            
        # Interface
        interface_info = config_dict["interfaces"]
        sub_info = interface_info["Subscribers"][0]
        if sub_info["Type"] == "zmq_tcp":
            self.cbbox_tcp_interface_subscriber_type.setCurrentText("TCP")
        else:
            self.cbbox_tcp_interface_subscriber_type.setCurrentText("IPC")
        self.le_tcp_interface_subscriber_endpoint.setText(sub_info["EndPoint"])

    def slot_switch_mode(self):
        interface_type = ""
        endpoint = ""

        if self.sender() == self.btn_vi_va:
            self.click_wgt_analytics.show()
            self.line_ingestion_analytics.show()
            self.btn_vi_va.setStyleSheet(SS_STD_BTN_ENABLE)
            self.btn_only_vi.setStyleSheet(SS_STD_BTN_DISABLE)
            self.vi_va_mode = "vi_va"

            # Analytics subscribes to ingestion
            viva_interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
            self.cbbox_analytics_interface_subscriber_type.setCurrentText(viva_interface_type)
            if viva_interface_type == "IPC":
                viva_endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
            else:
                viva_endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
            self.le_analytics_interface_subscriber_endpoint.setText(viva_endpoint)

            # Output modules subscribers to analytics
            interface_type = self.cbbox_analytics_interface_publisher_type.currentText()
            if interface_type == "IPC":
                endpoint = self.le_analytics_interface_publisher_endpoint_ipc.text()
            else:
                endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
        else:
            self.click_wgt_analytics.hide()
            self.line_ingestion_analytics.hide()
            self.btn_vi_va.setStyleSheet(SS_STD_BTN_DISABLE)
            self.btn_only_vi.setStyleSheet(SS_STD_BTN_ENABLE)
            self.vi_va_mode = "only_vi"

            # Output modules subscribers to ingestion
            interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
            if interface_type == "IPC":
                endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
            else:
                endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()

        # Output modules subscribers changes
        # type - cbbox
        self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
        self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
        self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
        # endpoint - le
        self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
        self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
        self.le_tcp_interface_subscriber_endpoint.setText(endpoint)
        
        # Activate ingestion wgt and deactivate others
        for item in self.all_clickable_wgts:
            if item == self.click_wgt_ingestion:
                item.setStyleSheet(SS_BIG_BTN_ENABLE)
            else:
                item.setStyleSheet(SS_BIG_BTN_DISABLE)

        # Activate ingestion configuration
        self.activate_module_configuration(self.click_wgt_ingestion)
    
    def activate_module_configuration(self, clickable_wgt):
        # TODO: reimplement later
        if clickable_wgt == self.click_wgt_ingestion:
            self.wgt_cfg_ingestion.show()
            
            self.wgt_cfg_analytics.hide()
            self.wgt_cfg_output_modules.hide()
        
        elif clickable_wgt == self.click_wgt_analytics:
            self.wgt_cfg_analytics.show()
            
            self.wgt_cfg_ingestion.hide()
            self.wgt_cfg_output_modules.hide()

        elif clickable_wgt == self.click_wgt_image_store:
            self.wgt_cfg_output_modules.show()
            
            self.wgt_cfg_ingestion.hide()
            self.wgt_cfg_analytics.hide()
    
    def slot_data_stream_name(self):
        activated = self.sender()
        activated_wgt = None
        
        for item in self.all_clickable_wgts:
            if item == activated:
                item.setStyleSheet(SS_BIG_BTN_ENABLE)
                activated_wgt = item
            else:
                item.setStyleSheet(SS_BIG_BTN_DISABLE)

        # Activate module configuration
        self.activate_module_configuration(activated_wgt)

    def _configuration_ingestion_source_is_video_file(self):
        self.ckbox_ingestion_ingestor_loop.show()
        self.lbl_ingestion_ingestor_video_file.show()
        self.le_ingestion_ingestor_video_file.show()
        self.btn_ingestion_ingestor_open_video_file.show()
        self.lbl_ingestion_ingestor_poll_interval.show()
        self.le_ingestion_ingestor_poll_interval.show()

        self.lbl_ingestion_ingestor_2d_camera_type.hide()
        self.cbbox_ingestion_ingestor_2d_camera_type.hide()
        self.btn_ingestion_ingestor_2d_camera_tuner.hide()
        self.lbl_ingestion_ingestor_pipeline.hide()
        self.le_ingestion_ingestor_pipeline.hide()

        self.lbl_ingestion_ingestor_3d_camera_type.hide()
        self.cbbox_ingestion_ingestor_3d_camera_type.hide()
        self.btn_ingestion_ingestor_realsense_viewer.hide()
        self.lbl_ingestion_ingestor_serial_number.hide()
        self.le_ingestion_ingestor_serial_number.hide()
        self.lbl_ingestion_ingestor_frame_rate.hide()
        self.le_ingestion_ingestor_frame_rate.hide()
        self.lbl_ingestion_ingestor_imu.hide()
        self.cbbox_ingestion_ingestor_imu.hide()

    def _configuration_ingestion_source_is_2d_camera(self):
        self.ckbox_ingestion_ingestor_loop.hide()
        self.lbl_ingestion_ingestor_video_file.hide()
        self.le_ingestion_ingestor_video_file.hide()
        self.btn_ingestion_ingestor_open_video_file.hide()

        self.lbl_ingestion_ingestor_2d_camera_type.show()
        self.cbbox_ingestion_ingestor_2d_camera_type.show()
        self.btn_ingestion_ingestor_2d_camera_tuner.show()
        self.lbl_ingestion_ingestor_pipeline.show()
        self.le_ingestion_ingestor_pipeline.show()
        self.lbl_ingestion_ingestor_poll_interval.show()
        self.le_ingestion_ingestor_poll_interval.show()

        self.lbl_ingestion_ingestor_3d_camera_type.hide()
        self.cbbox_ingestion_ingestor_3d_camera_type.hide()
        self.btn_ingestion_ingestor_realsense_viewer.hide()
        self.lbl_ingestion_ingestor_serial_number.hide()
        self.le_ingestion_ingestor_serial_number.hide()
        self.lbl_ingestion_ingestor_frame_rate.hide()
        self.le_ingestion_ingestor_frame_rate.hide()
        self.lbl_ingestion_ingestor_imu.hide()
        self.cbbox_ingestion_ingestor_imu.hide()

    def _configuration_ingestion_source_is_3d_camera(self):
        self.ckbox_ingestion_ingestor_loop.hide()
        self.lbl_ingestion_ingestor_video_file.hide()
        self.le_ingestion_ingestor_video_file.hide()
        self.btn_ingestion_ingestor_open_video_file.hide()
        self.lbl_ingestion_ingestor_poll_interval.hide()
        self.le_ingestion_ingestor_poll_interval.hide()

        self.lbl_ingestion_ingestor_2d_camera_type.hide()
        self.cbbox_ingestion_ingestor_2d_camera_type.hide()
        self.btn_ingestion_ingestor_2d_camera_tuner.hide()
        self.lbl_ingestion_ingestor_pipeline.hide()
        self.le_ingestion_ingestor_pipeline.hide()

        self.lbl_ingestion_ingestor_3d_camera_type.show()
        self.cbbox_ingestion_ingestor_3d_camera_type.show()
        self.btn_ingestion_ingestor_realsense_viewer.show()
        self.lbl_ingestion_ingestor_serial_number.show()
        self.le_ingestion_ingestor_serial_number.show()
        self.lbl_ingestion_ingestor_frame_rate.show()
        self.le_ingestion_ingestor_frame_rate.show()
        self.lbl_ingestion_ingestor_imu.show()
        self.cbbox_ingestion_ingestor_imu.show()
    
    def slot_configuration_ingestion_source_changed(self, source):
        if source == "Video File":
            self._configuration_ingestion_source_is_video_file()

        elif source == "2D Camera":
            self._configuration_ingestion_source_is_2d_camera()

        elif source == "3D Camera":
            self._configuration_ingestion_source_is_3d_camera()
    
    def slot_configuration_ingestion_open_video_file(self):
        eii_dep_tool_root = get_eiigui_root_path()
        udf_samples_path = os.path.join(eii_dep_tool_root, "eii_helper", "udf-samples")
        show_path = "/home"
        if os.path.exists(udf_samples_path):
            show_path = udf_samples_path
        else:
            show_path = "/home/{}".format(getpass.getuser())

        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fname = QFileDialog.getOpenFileName(self, "Select video file", 
            show_path, "Video Files (*.avi *.mp4 *.flv)", options=options)[0]
        if fname:
            self.le_ingestion_ingestor_video_file.setText(fname)
    
    def slot_configuration_ingestion_2d_camera_tuner(self):
        if self.cbbox_ingestion_ingestor_2d_camera_type.currentText() == "USB":
            self.configuration_ingestion_2d_camera_tuner = \
                UsbCameraTuner(output_wgt=self.le_ingestion_ingestor_pipeline)
            self.configuration_ingestion_2d_camera_tuner.exec_()
        elif self.cbbox_ingestion_ingestor_2d_camera_type.currentText() in ["GigE", "USB3.0"]:
            self.configuration_ingestion_2d_camera_tuner = \
                CameraTuner(output_wgt=self.le_ingestion_ingestor_pipeline, host_pwd=self.host_pwd)
            self.configuration_ingestion_2d_camera_tuner.exec_()
        elif self.cbbox_ingestion_ingestor_2d_camera_type.currentText() == "RTSP":
            self.configuration_ingestion_2d_camera_tuner = \
                RtspCameraTuner(output_wgt=self.le_ingestion_ingestor_pipeline)
            self.configuration_ingestion_2d_camera_tuner.exec_()
        else:
            pass

    def slot_configuration_ingestion_realsense_viewer(self):
        proc = sp.run("which realsense-viewer", shell=True, stdout=sp.PIPE)
        if not proc.stdout.decode():
            QMessageBox.warning(self, "Warning", 
                "The app \"realsense-viewer\" was not found.\nPlease install it first.",
                QMessageBox.Ok)
            return
        
        cmd = ["realsense-viewer"]
        sp.Popen(cmd, shell=False)
    
    def _configuration_ingestion_udf_add(self, parameter_name, value_type, parameter_value=None):
        self.counter_of_ingestion_udf_added_items += 1

        # Check the parameter duplication
        if parameter_name in self.ingestion_udf_added_keys.keys():
            res = QMessageBox.warning(self, "Warning",
                "The parameter \"{}\" is duplicate. Replace the previous one?".format(parameter_name),
                QMessageBox.Yes|QMessageBox.No)
            if res == QMessageBox.No:
                return
            else:
                # Remove the previous one
                prev_del_btn = self.ingestion_udf_added_keys[parameter_name]
                prev_lbl_param_name, prev_wgt_value_type = self.ingestion_udf_added_items[prev_del_btn]
                
                self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_lbl_param_name)
                self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_wgt_value_type)
                self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_del_btn)
                prev_lbl_param_name.deleteLater()
                prev_wgt_value_type.deleteLater()
                prev_del_btn.deleteLater()
                self.ingestion_udf_added_items.pop(prev_del_btn)
                self.ingestion_udf_added_keys.pop(parameter_name)

        # Parameter name
        lbl_parameter_name = KeyLabel(self.tab_cfg_ingestion_algorithm)
        lbl_parameter_name.setText(parameter_name)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(lbl_parameter_name, 
            self.counter_of_ingestion_udf_added_items, 1, 1, 1)
        # Value type
        if value_type != "Boolean":
            wgt_value_type = QLineEdit(self.tab_cfg_ingestion_algorithm)
            wgt_value_type.setPlaceholderText("Value type: {}".format(value_type))
            # Only basic validation
            if value_type == "Integer":
                wgt_value_type.setValidator(QIntValidator())
            elif value_type == "Floating":
                wgt_value_type.setValidator(QDoubleValidator())
            # For loading value from json file
            if parameter_value:
                wgt_value_type.setText(str(parameter_value))
        else:
            wgt_value_type = QComboBox(self.tab_cfg_ingestion_algorithm)
            wgt_value_type.addItems(["True", "False"])
            # For loading value from json file
            if parameter_value:
                wgt_value_type.setCurrentText(str(parameter_value))
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(wgt_value_type, 
            self.counter_of_ingestion_udf_added_items, 2, 1, 1)
        # Delete button
        del_button = CircleButton(char="-", parent=self.tab_cfg_ingestion_algorithm)
        self.grid_lyt_cfg_ingestion_algorithm.addWidget(del_button,
            self.counter_of_ingestion_udf_added_items, 3, 1, 1)
        del_button.clicked.connect(self.slot_configuration_ingestion_udf_del)
        
        # For travelling later to get value or delete widgets
        self.ingestion_udf_added_items[del_button] = (lbl_parameter_name, wgt_value_type)
        self.ingestion_udf_added_keys[parameter_name] = del_button
    
    def _configuration_analytics_udf_add(self, parameter_name, value_type, parameter_value=None):
        self.counter_of_analytics_udf_added_items += 1

        # Check the parameter duplication
        if parameter_name in self.analytics_udf_added_keys.keys():
            res = QMessageBox.warning(self, "Warning",
                "The parameter \"{}\" is duplicate. Replace the previous one?".format(parameter_name),
                QMessageBox.Yes|QMessageBox.No)
            if res == QMessageBox.No:
                return
            else:
                # Remove the previous one
                prev_del_btn = self.analytics_udf_added_keys[parameter_name]
                prev_lbl_param_name, prev_wgt_value_type = self.analytics_udf_added_items[prev_del_btn]
                
                self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_lbl_param_name)
                self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_wgt_value_type)
                self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_del_btn)
                prev_lbl_param_name.deleteLater()
                prev_wgt_value_type.deleteLater()
                prev_del_btn.deleteLater()
                self.analytics_udf_added_items.pop(prev_del_btn)
                self.analytics_udf_added_keys.pop(parameter_name)

        # Parameter name
        lbl_parameter_name = KeyLabel(self.tab_cfg_analytics_algorithm)
        lbl_parameter_name.setText(parameter_name)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(lbl_parameter_name, 
            self.counter_of_analytics_udf_added_items, 1, 1, 1)
        # Value type
        if value_type != "Boolean":
            wgt_value_type = QLineEdit(self.tab_cfg_analytics_algorithm)
            wgt_value_type.setPlaceholderText("Value type: {}".format(value_type))
            # Only basic validation
            if value_type == "Integer":
                wgt_value_type.setValidator(QIntValidator())
            elif value_type == "Floating":
                wgt_value_type.setValidator(QDoubleValidator())
            # For loading value from json file
            if parameter_value:
                wgt_value_type.setText(str(parameter_value))
        else:
            wgt_value_type = QComboBox(self.tab_cfg_analytics_algorithm)
            wgt_value_type.addItems(["True", "False"])
            # For loading value from json file
            if parameter_value:
                wgt_value_type.setCurrentText(str(parameter_value))
        self.grid_lyt_cfg_analytics_algorithm.addWidget(wgt_value_type, 
            self.counter_of_analytics_udf_added_items, 2, 1, 1)
        # Delete button
        del_button = CircleButton(char="-", parent=self.tab_cfg_analytics_algorithm)
        self.grid_lyt_cfg_analytics_algorithm.addWidget(del_button,
            self.counter_of_analytics_udf_added_items, 3, 1, 1)
        del_button.clicked.connect(self.slot_configuration_analytics_udf_del)
        
        # For travelling later to get value or delete widgets
        self.analytics_udf_added_items[del_button] = (lbl_parameter_name, wgt_value_type)
        self.analytics_udf_added_keys[parameter_name] = del_button
    
    def slot_configuration_ingestion_udf_add(self):
        cfg = {}
        win = TypeSelectorDialog(cfg)
        win.exec_()
        if not cfg:
            return
        parameter_name = cfg["parameter_name"]
        value_type = cfg["value_type"]

        self._configuration_ingestion_udf_add(parameter_name, value_type)

    def slot_configuration_analytics_udf_add(self):
        cfg = {}
        win = TypeSelectorDialog(cfg)
        win.exec_()
        if not cfg:
            return
        parameter_name = cfg["parameter_name"]
        value_type = cfg["value_type"]

        self._configuration_analytics_udf_add(parameter_name, value_type)
    
    def slot_configuration_ingestion_udf_del(self):
        del_btn = self.sender()
        lbl_parameter_name, wgt_value_type = self.ingestion_udf_added_items[del_btn]
        res = QMessageBox.warning(self, "Warning", 
            "Are you sure to remove the parameter \"{}\"?".format(lbl_parameter_name.text()), 
            QMessageBox.Ok|QMessageBox.Cancel)
        if res == QMessageBox.Cancel:
            return
        
        self.grid_lyt_cfg_ingestion_algorithm.removeWidget(lbl_parameter_name)
        self.grid_lyt_cfg_ingestion_algorithm.removeWidget(wgt_value_type)
        self.grid_lyt_cfg_ingestion_algorithm.removeWidget(del_btn)
        lbl_parameter_name.deleteLater()
        wgt_value_type.deleteLater()
        del_btn.deleteLater()
        self.ingestion_udf_added_items.pop(del_btn)
        self.ingestion_udf_added_keys.pop(lbl_parameter_name.text())

    def slot_configuration_analytics_udf_del(self):
        del_btn = self.sender()
        lbl_parameter_name, wgt_value_type = self.analytics_udf_added_items[del_btn]
        res = QMessageBox.warning(self, "Warning", 
            "Are you sure to remove the parameter \"{}\"?".format(lbl_parameter_name.text()), 
            QMessageBox.Ok|QMessageBox.Cancel)
        if res == QMessageBox.Cancel:
            return
        
        self.grid_lyt_cfg_analytics_algorithm.removeWidget(lbl_parameter_name)
        self.grid_lyt_cfg_analytics_algorithm.removeWidget(wgt_value_type)
        self.grid_lyt_cfg_analytics_algorithm.removeWidget(del_btn)
        lbl_parameter_name.deleteLater()
        wgt_value_type.deleteLater()
        del_btn.deleteLater()
        self.analytics_udf_added_items.pop(del_btn)
        self.analytics_udf_added_keys.pop(lbl_parameter_name.text())
    
    @staticmethod
    def _show_directory_dialog():
        eii_dep_tool_root = get_eiigui_root_path()
        udf_samples_path = os.path.join(eii_dep_tool_root, "eii_helper", "udf-samples")
        show_path = "/home"
        if os.path.exists(udf_samples_path):
            show_path = udf_samples_path
        else:
            show_path = "/home/{}".format(getpass.getuser())

        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        options |= QFileDialog.ShowDirsOnly
        selected_dir = str(QFileDialog.getExistingDirectory(
            None, "Select Directory", show_path, options=options))
        return selected_dir
    
    def _configuration_ingestion_udf_load_from_dict(self, udfs_dict):
        for k, v in udfs_dict.items():
            if k == "name":
                self.le_ingestion_algorithm_udf_name.setText(str(v))
            elif k == "type":
                if v == "python":
                    self.cbbox_ingestion_algorithm_udf_type.setCurrentText("Python")
                elif v == "native":
                    self.cbbox_ingestion_algorithm_udf_type.setCurrentText("Native")
                elif v == "raw_native":
                    self.cbbox_ingestion_algorithm_udf_type.setCurrentText("Raw_native")
                else:
                    QMessageBox.warning(self, "Warning", 
                        "Unsupported UDFs type \"{}\"".format(v), 
                        QMessageBox.Ok)
                    return
            else:
                if isinstance(v, int):
                    self._configuration_ingestion_udf_add(k, "Integer", v)
                elif isinstance(v, float):
                    self._configuration_ingestion_udf_add(k, "Floating", v)
                elif isinstance(v, bool):
                    self._configuration_ingestion_udf_add(k, "Boolean", v)
                elif isinstance(v, str):
                    self._configuration_ingestion_udf_add(k, "String", v)
                else:
                    QMessageBox.warning(self, "Warning", 
                        "Unsupported value type \"{}\" for value \"{}\"".format(type(v), v), 
                        QMessageBox.Ok)
                    return

    def _configuration_analytics_udf_load_from_dict(self, udfs_dict):
        for k, v in udfs_dict.items():
            if k == "name":
                self.le_analytics_algorithm_udf_name.setText(str(v))
            elif k == "type":
                if v == "python":
                    self.cbbox_analytics_algorithm_udf_type.setCurrentText("Python")
                elif v == "native":
                    self.cbbox_analytics_algorithm_udf_type.setCurrentText("Native")
                elif v == "raw_native":
                    self.cbbox_analytics_algorithm_udf_type.setCurrentText("Raw_native")
                else:
                    QMessageBox.warning(self, "Warning", 
                        "Unsupported UDFs type \"{}\"".format(v), 
                        QMessageBox.Ok)
                    return
            else:
                if isinstance(v, int):
                    self._configuration_analytics_udf_add(k, "Integer", v)
                elif isinstance(v, float):
                    self._configuration_analytics_udf_add(k, "Floating", v)
                elif isinstance(v, bool):
                    self._configuration_analytics_udf_add(k, "Boolean", v)
                elif isinstance(v, str):
                    self._configuration_analytics_udf_add(k, "String", v)
                else:
                    QMessageBox.warning(self, "Warning", 
                        "Unsupported value type \"{}\" for value \"{}\"".format(type(v), v), 
                        QMessageBox.Ok)
                    return
    
    def slot_configuration_ingestion_udf_import(self):
        selected_dir = self._show_directory_dialog()
        if selected_dir == "":
            return
        
        # Check for udfs and dockerfile
        udfs_dir = os.path.join(selected_dir, "udfs")
        if not os.path.exists(udfs_dir):
            QMessageBox.warning(self, "Warning", 
                "No \"udfs\" folder was found in path:\n{}".format(selected_dir), 
                QMessageBox.Ok)
            return
        dockerfile = os.path.join(selected_dir, "Dockerfile")
        if not os.path.exists(dockerfile):
            QMessageBox.warning(self, "Warning", 
                "No \"Dockerfile\" file was found in path:\n{}".format(selected_dir), 
                QMessageBox.Ok)
            return

        # If udfs.json was found, load it
        udfs_json = os.path.join(selected_dir, "udfs.json")
        if os.path.exists(udfs_json):
            with open(udfs_json, "r") as f:
                udfs_dict = json.loads(f.read())
            self._configuration_ingestion_udf_load_from_dict(udfs_dict)

        # If labels.json was found, load it
        labels_json = os.path.join(selected_dir, "labels.json")
        if os.path.exists(labels_json):
            with open(labels_json, "r") as f:
                labels_dict = json.loads(f.read())
            self.udfs_labels = labels_dict

        # Update the udfs source path
        self.ingestion_udfs_path = selected_dir
    
    def slot_configuration_ingestion_udf_reset(self):
        self.cbbox_ingestion_algorithm_udf_type.setCurrentIndex(0)
        self.le_ingestion_algorithm_udf_name.setText("")
        self.le_ingestion_algorithm_udf_max_workers.setText("4")
        
        # Remove udf parameters
        if not self.ingestion_udf_added_keys.keys() or not self.ingestion_udf_added_items.keys():
            print("Empty ingestion udf params")
            return
        for parameter_name in self.ingestion_udf_added_keys.keys():
            # Remove from UI
            prev_del_btn = self.ingestion_udf_added_keys[parameter_name]
            prev_lbl_param_name, prev_wgt_value_type = self.ingestion_udf_added_items[prev_del_btn]
                
            self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_lbl_param_name)
            self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_wgt_value_type)
            self.grid_lyt_cfg_ingestion_algorithm.removeWidget(prev_del_btn)
            prev_lbl_param_name.deleteLater()
            prev_wgt_value_type.deleteLater()
            prev_del_btn.deleteLater()
        self.ingestion_udf_added_keys.clear()
        self.ingestion_udf_added_items.clear()

    def slot_configuration_analytics_udf_import(self):
        selected_dir = self._show_directory_dialog()
        if selected_dir == "":
            return
        
        # Check for udfs and dockerfile
        udfs_dir = os.path.join(selected_dir, "udfs")
        if not os.path.exists(udfs_dir):
            QMessageBox.warning(self, "Warning", 
                "No \"udfs\" folder was found in path:\n{}".format(selected_dir), 
                QMessageBox.Ok)
            return
        dockerfile = os.path.join(selected_dir, "Dockerfile")
        if not os.path.exists(dockerfile):
            QMessageBox.warning(self, "Warning", 
                "No \"Dockerfile\" file was found in path:\n{}".format(selected_dir), 
                QMessageBox.Ok)
            return

        # If udfs.json was found, load it
        udfs_json = os.path.join(selected_dir, "udfs.json")
        if os.path.exists(udfs_json):
            with open(udfs_json, "r") as f:
                udfs_dict = json.loads(f.read())
            self._configuration_analytics_udf_load_from_dict(udfs_dict)

        # Update the udfs source path
        self.analytics_udfs_path = selected_dir

    def slot_configuration_analytics_udf_reset(self):
        self.cbbox_analytics_algorithm_udf_type.setCurrentIndex(0)
        self.le_analytics_algorithm_udf_name.setText("")
        self.le_analytics_algorithm_udf_max_workers.setText("4")
        
        # Remove udf parameters
        if not self.analytics_udf_added_keys.keys() or not self.analytics_udf_added_items.keys():
            print("Empty analytics udf params")
            return
        for parameter_name in self.analytics_udf_added_keys.keys():
            # Remove from UI
            prev_del_btn = self.analytics_udf_added_keys[parameter_name]
            prev_lbl_param_name, prev_wgt_value_type = self.analytics_udf_added_items[prev_del_btn]
                
            self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_lbl_param_name)
            self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_wgt_value_type)
            self.grid_lyt_cfg_analytics_algorithm.removeWidget(prev_del_btn)
            prev_lbl_param_name.deleteLater()
            prev_wgt_value_type.deleteLater()
            prev_del_btn.deleteLater()
        self.analytics_udf_added_keys.clear()
        self.analytics_udf_added_items.clear()
    
    def slot_configuration_ingestion_udf_checked(self, checked):
        if checked:
            self._show_configuration_ingestion_udf_items()
        else:
            self._hide_configuration_ingestion_udf_items()

    def slot_configuration_analytics_udf_checked(self, checked):
        if checked:
            self._show_configuration_analytics_udf_items()
        else:
            self._hide_configuration_analytics_udf_items()
    
    def slot_configuration_enable_image_store_checked(self, checked):
        if checked:
            self._show_configuration_image_store_items()
        else:
            self._hide_configuration_image_store_items()

    def slot_configuration_enable_opcua_checked(self, checked):
        if checked:
            self._show_configuration_opcua_items()
        else:
            self._hide_configuration_opcua_items()

    def slot_configuration_enable_tcp_checked(self, checked):
        if checked:
            self._show_configuration_tcp_items()
        else:
            self._hide_configuration_tcp_items()

    # VI show/hide
    def _hide_configuration_ingestion_udf_items(self):
        self.cbbox_ingestion_algorithm_udf_type.hide()
        self.btn_ingestion_algorithm_import_udf.hide()
        self.btn_ingestion_algorithm_reset_udf.hide()
        self.lbl_ingestion_algorithm_udf_type.hide()
        self.cbbox_ingestion_algorithm_udf_type.hide()
        self.lbl_ingestion_algorithm_udf_name.hide()
        self.le_ingestion_algorithm_udf_name.hide()
        self.lbl_ingestion_algorithm_udf_max_workers.hide()
        self.le_ingestion_algorithm_udf_max_workers.hide()
        self.btn_ingestion_algorithm_udf_add.hide()
        for del_btn, (lbl_param_name, wgt_value_type) in self.ingestion_udf_added_items.items():
            del_btn.hide()
            lbl_param_name.hide()
            wgt_value_type.hide()

    def _show_configuration_ingestion_udf_items(self):
        self.cbbox_ingestion_algorithm_udf_type.show()
        self.btn_ingestion_algorithm_import_udf.show()
        self.btn_ingestion_algorithm_reset_udf.show()
        self.lbl_ingestion_algorithm_udf_type.show()
        self.cbbox_ingestion_algorithm_udf_type.show()
        self.lbl_ingestion_algorithm_udf_name.show()
        self.le_ingestion_algorithm_udf_name.show()
        self.lbl_ingestion_algorithm_udf_max_workers.show()
        self.le_ingestion_algorithm_udf_max_workers.show()
        self.btn_ingestion_algorithm_udf_add.show()
        for del_btn, (lbl_param_name, wgt_value_type) in self.ingestion_udf_added_items.items():
            del_btn.show()
            lbl_param_name.show()
            wgt_value_type.show()
    
    # VA show/hide
    def _hide_configuration_analytics_udf_items(self):
        self.cbbox_analytics_algorithm_udf_type.hide()
        self.btn_analytics_algorithm_import_udf.hide()
        self.btn_analytics_algorithm_reset_udf.hide()
        self.lbl_analytics_algorithm_udf_type.hide()
        self.cbbox_analytics_algorithm_udf_type.hide()
        self.lbl_analytics_algorithm_udf_name.hide()
        self.le_analytics_algorithm_udf_name.hide()
        self.lbl_analytics_algorithm_udf_max_workers.hide()
        self.le_analytics_algorithm_udf_max_workers.hide()
        self.btn_analytics_algorithm_udf_add.hide()
        for del_btn, (lbl_param_name, wgt_value_type) in self.analytics_udf_added_items.items():
            del_btn.hide()
            lbl_param_name.hide()
            wgt_value_type.hide()

    def _show_configuration_analytics_udf_items(self):
        self.cbbox_analytics_algorithm_udf_type.show()
        self.btn_analytics_algorithm_import_udf.show()
        self.btn_analytics_algorithm_reset_udf.show()
        self.lbl_analytics_algorithm_udf_type.show()
        self.cbbox_analytics_algorithm_udf_type.show()
        self.lbl_analytics_algorithm_udf_name.show()
        self.le_analytics_algorithm_udf_name.show()
        self.lbl_analytics_algorithm_udf_max_workers.show()
        self.le_analytics_algorithm_udf_max_workers.show()
        self.btn_analytics_algorithm_udf_add.show()
        for del_btn, (lbl_param_name, wgt_value_type) in self.analytics_udf_added_items.items():
            del_btn.show()
            lbl_param_name.show()
            wgt_value_type.show()

    # Image Store show/hide
    def _show_configuration_image_store_items(self):
        self.lbl_imagestore_storage_policy.show()
        self.lbl_imagestore_minio_retention_time.show()
        self.le_imagestore_minio_retention_time.show()
        self.lbl_imagestore_minio_retention_poll_interval.show()
        self.le_imagestore_minio_retention_poll_interval.show()
        self.lbl_imagestore_interface.show()
        self.lbl_imagestore_interface_subscriber.show()
        self.lbl_imagestore_interface_subscriber_type.show()
        self.cbbox_imagestore_interface_subscriber_type.show()
        self.lbl_imagestore_interface_subscriber_endpoint.show()
        self.le_imagestore_interface_subscriber_endpoint.show()
        self.lbl_imagestore_interface_server.show()
        self.lbl_imagestore_interface_server_type.show()
        self.cbbox_imagestore_interface_server_type.show()
        if self.cbbox_imagestore_interface_server_type.currentText == "ICP":
            self.lbl_imagestore_interface_server_endpoint_ipc.show()
            self.le_imagestore_interface_server_endpoint_ipc.show()
        else:
            self.lbl_imagestore_interface_server_endpoint_tcp.show()
            self.le_imagestore_interface_server_endpoint_tcp.show()

    def _hide_configuration_image_store_items(self):
        self.lbl_imagestore_storage_policy.hide()
        self.lbl_imagestore_minio_retention_time.hide()
        self.le_imagestore_minio_retention_time.hide()
        self.lbl_imagestore_minio_retention_poll_interval.hide()
        self.le_imagestore_minio_retention_poll_interval.hide()
        self.lbl_imagestore_interface.hide()
        self.lbl_imagestore_interface_subscriber.hide()
        self.lbl_imagestore_interface_subscriber_type.hide()
        self.cbbox_imagestore_interface_subscriber_type.hide()
        self.lbl_imagestore_interface_subscriber_endpoint.hide()
        self.le_imagestore_interface_subscriber_endpoint.hide()
        self.lbl_imagestore_interface_server.hide()
        self.lbl_imagestore_interface_server_type.hide()
        self.cbbox_imagestore_interface_server_type.hide()
        self.lbl_imagestore_interface_server_endpoint_ipc.hide()
        self.le_imagestore_interface_server_endpoint_ipc.hide()
        self.lbl_imagestore_interface_server_endpoint_tcp.hide()
        self.le_imagestore_interface_server_endpoint_tcp.hide()

    # OPCUA show/hide
    def _show_configuration_opcua_items(self):
        self.lbl_opcua_config.show()
        self.lbl_opcua_topics.show()
        self.le_opcua_topics.show()
        self.lbl_opcua_address.show()
        self.le_opcua_address.show()
        self.lbl_opcua_interface.show()
        self.lbl_opcua_interface_subscriber.show()
        self.lbl_opcua_interface_subscriber_type.show()
        self.cbbox_opcua_interface_subscriber_type.show()
        self.lbl_opcua_interface_subscriber_endpoint.show()
        self.le_opcua_interface_subscriber_endpoint.show()

    def _hide_configuration_opcua_items(self):
        self.lbl_opcua_config.hide()
        self.lbl_opcua_topics.hide()
        self.le_opcua_topics.hide()
        self.lbl_opcua_address.hide()
        self.le_opcua_address.hide()
        self.lbl_opcua_interface.hide()
        self.lbl_opcua_interface_subscriber.hide()
        self.lbl_opcua_interface_subscriber_type.hide()
        self.cbbox_opcua_interface_subscriber_type.hide()
        self.lbl_opcua_interface_subscriber_endpoint.hide()
        self.le_opcua_interface_subscriber_endpoint.hide()

    # TCP show/hide
    def _show_configuration_tcp_items(self):
        self.lbl_tcp_config.show()
        self.lbl_tcp_ip.show()
        self.le_tcp_ip.show()
        self.lbl_tcp_port.show()
        self.le_tcp_port.show()
        self.lbl_tcp_interface.show()
        self.lbl_tcp_interface_subscriber.show()
        self.lbl_tcp_interface_subscriber_type.show()
        self.cbbox_tcp_interface_subscriber_type.show()
        self.lbl_tcp_interface_subscriber_endpoint.show()
        self.le_tcp_interface_subscriber_endpoint.show()

    def _hide_configuration_tcp_items(self):
        self.lbl_tcp_config.hide()
        self.lbl_tcp_ip.hide()
        self.le_tcp_ip.hide()
        self.lbl_tcp_port.hide()
        self.le_tcp_port.hide()
        self.lbl_tcp_interface.hide()
        self.lbl_tcp_interface_subscriber.hide()
        self.lbl_tcp_interface_subscriber_type.hide()
        self.cbbox_tcp_interface_subscriber_type.hide()
        self.lbl_tcp_interface_subscriber_endpoint.hide()
        self.le_tcp_interface_subscriber_endpoint.hide()

    
    def slot_configuration_ingestion_interface_changed(self, interface_type):
        # Server
        if self.sender() == self.cbbox_ingestion_interface_server_type:
            if interface_type == "TCP":
                self.lbl_ingestion_interface_server_endpoint_tcp.show()
                self.le_ingestion_interface_server_endpoint_tcp.show()
                self.lbl_ingestion_interface_server_endpoint_ipc.hide()
                self.le_ingestion_interface_server_endpoint_ipc.hide()
            else:
                self.lbl_ingestion_interface_server_endpoint_ipc.show()
                self.le_ingestion_interface_server_endpoint_ipc.show()
                self.lbl_ingestion_interface_server_endpoint_tcp.hide()
                self.le_ingestion_interface_server_endpoint_tcp.hide()
        # Publisher
        else:
            if interface_type == "TCP":
                self.lbl_ingestion_interface_publisher_endpoint_tcp.show()
                self.le_ingestion_interface_publisher_endpoint_tcp.show()
                self.lbl_ingestion_interface_publisher_endpoint_ipc.hide()
                self.le_ingestion_interface_publisher_endpoint_ipc.hide()
            else:
                self.lbl_ingestion_interface_publisher_endpoint_ipc.show()
                self.le_ingestion_interface_publisher_endpoint_ipc.show()
                self.lbl_ingestion_interface_publisher_endpoint_tcp.hide()
                self.le_ingestion_interface_publisher_endpoint_tcp.hide()

    def slot_configuration_analytics_interface_changed(self, interface_type):
        if interface_type == "TCP":
            self.lbl_analytics_interface_publisher_endpoint_tcp.show()
            self.le_analytics_interface_publisher_endpoint_tcp.show()
            self.lbl_analytics_interface_publisher_endpoint_ipc.hide()
            self.le_analytics_interface_publisher_endpoint_ipc.hide()
        else:
            self.lbl_analytics_interface_publisher_endpoint_ipc.show()
            self.le_analytics_interface_publisher_endpoint_ipc.show()
            self.lbl_analytics_interface_publisher_endpoint_tcp.hide()
            self.le_analytics_interface_publisher_endpoint_tcp.hide()

    def slot_configuration_imagestore_interface_changed(self, interface_type):
        if interface_type == "TCP":
            self.lbl_imagestore_interface_server_endpoint_tcp.show()
            self.le_imagestore_interface_server_endpoint_tcp.show()
            self.lbl_imagestore_interface_server_endpoint_ipc.hide()
            self.le_imagestore_interface_server_endpoint_ipc.hide()
        else:
            self.lbl_imagestore_interface_server_endpoint_ipc.show()
            self.le_imagestore_interface_server_endpoint_ipc.show()
            self.lbl_imagestore_interface_server_endpoint_tcp.hide()
            self.le_imagestore_interface_server_endpoint_tcp.hide()
    
    def slot_configuration_ingestion_interface_publisher_changed(self):
        # Combobox is activated
        if self.sender() == self.cbbox_ingestion_interface_publisher_type:
            # Synchronize the changes to video analytics
            if self.vi_va_mode == "vi_va":
                interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
                self.cbbox_analytics_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_analytics_interface_subscriber_endpoint.setText(endpoint)
            
            # Synchronize the changes to other modules, like image store
            else:
                # Output modules
                interface_type = self.cbbox_ingestion_interface_publisher_type.currentText()
                self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
                self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
                self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
                if interface_type == "IPC":
                    endpoint = self.le_ingestion_interface_publisher_endpoint_ipc.text()
                else:
                    endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
                self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
                self.le_tcp_interface_subscriber_endpoint.setText(endpoint)
        
        # LineEdit is activated
        elif self.sender() == self.le_ingestion_interface_publisher_endpoint_tcp:
            # Synchronize the changes to video analytics
            if self.vi_va_mode == "vi_va":
                self.cbbox_analytics_interface_subscriber_type.setCurrentText("TCP")
                endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_analytics_interface_subscriber_endpoint.setText(endpoint)
            
            # Synchronize the changes to other modules, like image store
            else:
                interface_type = "TCP"
                self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
                self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
                self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
                endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
                self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
                self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
                self.le_tcp_interface_subscriber_endpoint.setText(endpoint)

    def slot_configuration_analytics_interface_publisher_changed(self):
        # Combobox is activated
        if self.sender() == self.cbbox_analytics_interface_publisher_type:
            # Synchronize the changes to other modules, like image store
            interface_type = self.cbbox_analytics_interface_publisher_type.currentText()
            self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
            self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
            self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
            if interface_type == "IPC":
                endpoint = self.le_analytics_interface_publisher_endpoint_ipc.text()
            else:
                endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
            self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
            self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
            self.le_tcp_interface_subscriber_endpoint.setText(endpoint)
        
        # LineEdit is activated
        elif self.sender() == self.le_analytics_interface_publisher_endpoint_tcp:
            # Synchronize the changes to other modules, like image store
            interface_type = "TCP"
            self.cbbox_imagestore_interface_subscriber_type.setCurrentText(interface_type)
            self.cbbox_opcua_interface_subscriber_type.setCurrentText(interface_type)
            self.cbbox_tcp_interface_subscriber_type.setCurrentText(interface_type)
            endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
            self.le_imagestore_interface_subscriber_endpoint.setText(endpoint)
            self.le_opcua_interface_subscriber_endpoint.setText(endpoint)
            self.le_tcp_interface_subscriber_endpoint.setText(endpoint)
    
    def slot_config_next(self):
        complete_config = {}

        # Video Ingestion
        ingestion_config = self._generate_ingestion_config()
        if ingestion_config is None:
            return
        complete_config[EII_MODULE.VI.value] = ingestion_config
        
        # Video Analytics
        if self.vi_va_mode == "vi_va":
            analytics_config = self._generate_analytics_config()
            if analytics_config is None:
                return
            complete_config[EII_MODULE.VA.value] = analytics_config

        # Image Store
        if self.ckbox_enable_image_store.isChecked():
            imagestore_config = self._generate_imagestore_config()
            if imagestore_config is None:
                return
            complete_config[EII_MODULE.ImageStore.value] = imagestore_config

        # OPCUA
        if self.ckbox_enable_opcua.isChecked():
            opcua_config = self._generate_opcua_config()
            if opcua_config is None:
                return
            complete_config[EII_MODULE.OPCUA.value] = opcua_config

        # TCP
        if self.ckbox_enable_tcp.isChecked():
            tcp_config = self._generate_tcp_config()
            if tcp_config is None:
                return
            complete_config[EII_MODULE.TCP.value] = tcp_config

        
        # Modules that are not configured by users
        # Web visualizer
        webvisualizer_config = {
            "dev_port": 5001,
            "draw_results": "true",
            "labels": {},
            "password": "admin@123",
            "port": 5000,
            "username": "admin"
        }
        webvisualizer_interfaces = {}
        subscriber = {}
        if self.vi_va_mode == "vi_va":
            interface_type = analytics_config["interfaces"]["Publishers"][0]["Type"]
            endpoint = analytics_config["interfaces"]["Publishers"][0]["EndPoint"]
            publisher_app_name = self.analytics_app_name
            topics = [self.analytics_topic_name]
            subscriber["Name"] = "default"
            subscriber["Type"] = interface_type
            subscriber["EndPoint"] = endpoint
            subscriber["PublisherAppName"] = publisher_app_name
            subscriber["Topics"] = topics
            webvisualizer_interfaces["Subscribers"] = [subscriber]
            cur_labels_path = os.path.join(self.project_path, "VideoAnalytics")
            new_pjt_tag = self.analytics_udfs_path
        else:
            interface_type = ingestion_config["interfaces"]["Publishers"][0]["Type"]
            endpoint = ingestion_config["interfaces"]["Publishers"][0]["EndPoint"]
            publisher_app_name = self.ingestion_app_name
            topics = [self.ingestion_topic_name]
            subscriber["Name"] = "default"
            subscriber["Type"] = interface_type
            subscriber["EndPoint"] = endpoint
            subscriber["PublisherAppName"] = publisher_app_name
            subscriber["Topics"] = topics
            webvisualizer_interfaces["Subscribers"] = [subscriber]
            cur_labels_path = os.path.join(self.project_path, "VideoIngestion")
            new_pjt_tag = self.ingestion_udfs_path

        if not os.path.exists(self.project_path)or not os.listdir(self.project_path):
            labels_json_path = os.path.join(new_pjt_tag, "labels.json")
        else:
            labels_json_path = os.path.join(cur_labels_path, "labels.json")

        if os.path.exists(labels_json_path):
            with open(labels_json_path, "r") as f:
                labels_dict = json.loads(f.read())
            udfs_labels = labels_dict
            if udfs_labels:
                webvisualizer_config["labels"] = {topics[0]: udfs_labels}
        complete_config["WebVisualizer"] = {
            "config": webvisualizer_config,
            "interfaces": webvisualizer_interfaces
        }

        # Prepare many files
        self._clean_workspace(complete_config)
        self._copy_templates_to_project_home(complete_config)
        self._generate_module_config_files(complete_config)
        self._generate_module_compose_files(complete_config)
        self._generate_project_components_file(complete_config)
        self._copy_udfs_files_to_project_home()
        self._overwrite_extraessential_modules_config()

        return complete_config
    
    def _clean_workspace(self, module_config):
        module_list = set(module_config.keys())
        all_module = set(EII_MODULE._value2member_map_.keys())
        extra_module = all_module - module_list
        for module in extra_module:
            module_dst_dir = os.path.join(self.project_path, module)
            if os.path.exists(module_dst_dir):
                shutil.rmtree(module_dst_dir)
    
    def _copy_templates_to_project_home(self, module_config):

        # Templates path
        eii_dep_tool_root = get_eiigui_root_path()
        temp_dir = os.path.join(eii_dep_tool_root, "eii_helper/templates/services")

        for module_name in module_config.keys():
            # dst path
            module_dst_dir = os.path.join(self.project_path, module_name)
            if not os.path.exists(module_dst_dir):
                # OPCUA module will reuse eii original module
                if module_name == EII_MODULE.OPCUA.value:
                    # Copy eii original module first
                    eii_opcua_dir = os.path.join(self.eii_home, module_name)
                    shutil.copytree(eii_opcua_dir, module_dst_dir)
                    # Temp file replace
                    opcua_temp_path = os.path.join(temp_dir, module_name)
                    file_list = os.listdir(opcua_temp_path)
                    for item in file_list:
                        item_path = os.path.join(opcua_temp_path, item)
                        target_path = os.path.join(module_dst_dir, item)
                        if os.path.isfile(item_path):
                            shutil.copy(item_path, target_path)
                        elif os.path.isdir(item_path):
                            if os.path.exists(target_path):
                                shutil.rmtree(target_path)
                            shutil.copytree(item_path, target_path)
                else:
                    module_temp_dir = os.path.join(temp_dir, module_name)
                    shutil.copytree(module_temp_dir, module_dst_dir)
    
    def _generate_module_config_files(self, module_config):
        # Remove the contents
        # cmd = "rm -rf {}".format(os.path.join(self.project_path, "*"))
        # sp.call(cmd, shell=True)
        
        for module_name, module_cfg in module_config.items():
            # Create module dir
            module_dir = os.path.join(self.project_path, module_name)
            # os.makedirs(module_dir, exist_ok=True)
            # Write config into config.json
            config_file = os.path.join(module_dir, "config.json")
            config_content = json.dumps(module_cfg, indent=4)
            with open(config_file, "w") as f:
                f.write(config_content)

    def _generate_module_compose_files(self, module_config):
        
        for module_name in module_config.keys():
            dst_compose_file = os.path.join(self.project_path, module_name, EII_FILE_NAME.DOCKER_COMPOSE.value)
            if os.path.exists(dst_compose_file):
                # Render compose file
                render_dict = {
                    "F_PROJECT_NAME": self.project_name,
                    "F_ETCD_HOST": self.eii_etcd_host,
                }
                with open(dst_compose_file, "r") as f:
                    content = f.read()
                    res = jinja2.Template(content).render(render_dict)
                with open(dst_compose_file, "w") as f:
                    f.write(res)

    def _generate_project_components_file(self, module_config):
        module_names = list(module_config.keys())
        content = "AppContexts:\n"
        content += "- EtcdUI\n"
        content += "- WebVisualizer\n"
        if EII_MODULE.ImageStore.value in module_names:
            content += "- ImageStore\n"
        content += "- eii_helper/workspace/{}/VideoIngestion\n".format(self.project_name)
        if EII_MODULE.VA.value in module_names:
            content += "- eii_helper/workspace/{}/VideoAnalytics\n".format(self.project_name)
        if EII_MODULE.OPCUA.value in module_names:
            content += "- eii_helper/workspace/{}/OpcuaExport\n".format(self.project_name)
        if EII_MODULE.TCP.value in module_names:
            content += "- eii_helper/workspace/{}/TcpExport\n".format(self.project_name)

        dst_path = os.path.join(self.project_path, EII_FILE_NAME.PROJECT_COMP.value)
        with open(dst_path, "w") as f:
            f.write(content)

    def _copy_udfs_files_to_project_home(self):
        # Copy contents only from imported udfs folders
        module_name = "VideoIngestion"
        if self.ckbox_ingestion_algorithm_udf.isChecked() and self.ingestion_udfs_path:
            src_udfs_path = os.path.join(self.ingestion_udfs_path)
            dst_udfs_path = os.path.join(self.project_path, module_name)
            copy_files_under_dir(src_udfs_path, dst_udfs_path)
        
        # If ingestor source is video file, copy the video file
        if self.cbbox_ingestion_ingestor_source.currentText() == "Video File":
            src_test_video = self.le_ingestion_ingestor_video_file.text()
            if src_test_video and os.path.exists(src_test_video):
                basename = os.path.basename(src_test_video)
                dst_test_video_dir = os.path.join(self.project_path, module_name, 
                    "test_videos")
                dst_test_video = os.path.join(dst_test_video_dir, basename)
                if not os.path.exists(dst_test_video):
                    os.makedirs(dst_test_video_dir, exist_ok=True)
                    shutil.copy(src_test_video, dst_test_video)
        
        module_name = "VideoAnalytics"
        if self.ckbox_analytics_algorithm_udf.isChecked() and self.analytics_udfs_path:
            src_udfs_path = os.path.join(self.analytics_udfs_path)
            dst_udfs_path = os.path.join(self.project_path, module_name)
            copy_files_under_dir(src_udfs_path, dst_udfs_path)

    def _overwrite_extraessential_modules_config(self):
        # Overwrite WebVisualizer/ImageStore default config.json and docker-compose.yml
        module_names = ["WebVisualizer"]
        if self.ckbox_enable_image_store.isChecked():
            module_names.append("ImageStore")
        
        for module_name in module_names:
            src_module_config = os.path.join(self.project_path, module_name)
            dst_module_config = os.path.join(self.eii_home, module_name)
            copy_files_under_dir(src_module_config, dst_module_config)
    
    def _warning_parameter_missing_value(self, module_name, item_name, param_name):
        QMessageBox.warning(self, "Warning", 
            "The parameter \"{}\" in \n{} > Configuration > {}\nshould not be empty.".format(
                param_name, module_name, item_name
            ),
            QMessageBox.Ok)

    def _warning_parameter_value_type_error(self, module_name, item_name, param_name, param_value, param_type):
        QMessageBox.warning(self, "Warning", 
            "The input value \"{}\" of parameter \"{}\" in \n{} > Configuration > {}\ncannot be converted to \"{}\".".format(
                param_value, param_name, module_name, item_name, param_type
            ),
            QMessageBox.Ok)
    
    @staticmethod
    def _is_integer(data_str):
        try:
            int(data_str)
        except ValueError:
            return False
        return True

    @staticmethod
    def _is_float(data_str):
        try:
            float(data_str)
        except ValueError:
            return False
        return True

    def _generate_ingestion_config(self):
        # Contain two parts, config and interfaces
        total_configuration = {}

        # ------------------------------- config ------------------------------- #
        module_config = {}
        # ------------------------------- config: ingestor ------------------------------- #
        ingestor = {}
        source = self.cbbox_ingestion_ingestor_source.currentText()
        if source == "Video File":
            # Make sure the video file exists
            video_file = self.le_ingestion_ingestor_video_file.text()
            if video_file == "":
                self._warning_parameter_missing_value("Video Ingestion", "Ingestor", "Video File")
                return
            # if not os.path.exists(video_file):
            #     QMessageBox.warning(self, "Warning", 
            #         "The input video file \"{}\" does not exist.".format(video_file),
            #         QMessageBox.Ok)
            #     return
            # Get basename of the video file
            video_file_basename = video_file.split("/")[-1]
            
            ingestor["type"] = "opencv"
            ingestor["pipeline"] = "./test_videos/{}".format(video_file_basename)
            ingestor["loop_video"] = True if self.ckbox_ingestion_ingestor_loop.isChecked() else False
            ingestor["queue_size"] = 10
            if self.le_ingestion_ingestor_poll_interval.text() == "":
                ingestor["poll_interval"] = 0.0
            else:
                ingestor["poll_interval"] = float(self.le_ingestion_ingestor_poll_interval.text())
        
        elif source == "2D Camera":
            gst_pipeline = self.le_ingestion_ingestor_pipeline.text()
            if gst_pipeline == "":
                self._warning_parameter_missing_value("Video Ingestion", "Ingestor", "Ingestion Pipeline")
                return

            ingestor["type"] = "gstreamer"
            ingestor["pipeline"] = gst_pipeline
            ingestor["queue_size"] = 10
            if self.le_ingestion_ingestor_poll_interval.text() == "":
                ingestor["poll_interval"] = 0.0
            else:
                ingestor["poll_interval"] = float(self.le_ingestion_ingestor_poll_interval.text())
        
        elif source == "3D Camera":
            ingestor["type"] = "realsense"
            ingestor["queue_size"] = 10
            serial_number = self.le_ingestion_ingestor_serial_number.text()
            # If serial number is an empty string, 
            # video ingestion will try to find the first one device
            if serial_number:
                ingestor["serial"] = serial_number
            frame_rate = self.le_ingestion_ingestor_frame_rate.text()
            if frame_rate:
                ingestor["framerate"] = int(frame_rate) if int(frame_rate) > 0 else 30
            if self.cbbox_ingestion_ingestor_imu.currentText() == "Off":
                ingestor["imu_on"] = False
            else:
                ingestor["imu_on"] = True
        
        else:
            QMessageBox.warning(self, "Warning", 
                "Unkown value \"{}\"".format(source), QMessageBox.Ok)
            return
        module_config["ingestor"] = ingestor

        # ------------------------------- config: encoding ------------------------------- #
        encoding = {"type": "jpeg", "level": 95}
        module_config["encoding"] = encoding

        # ------------------------------- config: sw_trigger ------------------------------- #
        sw_trigger = {}
        # if self.cbbox_ingestion_ingestor_sw_trigger.currentText() == "True":
        #     sw_trigger["init_state"] = "running"
        # else:
        #     sw_trigger["init_state"] = "stopped"
        sw_trigger["init_state"] = "running"
        module_config["sw_trigger"] = sw_trigger

        # ------------------------------- config: udfs ------------------------------- #
        udfs = []
        if not self.ckbox_ingestion_algorithm_udf.isChecked():
            module_config["udfs"] = udfs
        else:
            udf = {}
            udf_name = self.le_ingestion_algorithm_udf_name.text()
            if udf_name == "":
                self._warning_parameter_missing_value("Video Ingestion", "Algorithm", "Name")
                return
            udf["name"] = udf_name
            udf["type"] = self.cbbox_ingestion_algorithm_udf_type.currentText().lower()
            for _, (lbl_param_name, wgt_value_type) in self.ingestion_udf_added_items.items():
                param_name = lbl_param_name.text()
                # LineEdit
                if isinstance(wgt_value_type, QLineEdit):
                    param_value = wgt_value_type.text()
                    # Make sure the value is not an empty string
                    if param_value == "":
                        self._warning_parameter_missing_value("Video Ingestion", "Algorithm", param_name)
                        return
                    
                    # Make sure the type of input data is same as the value of TypeSelector
                    value_type = wgt_value_type.placeholderText().split(":")[-1].strip()
                    if value_type == "Integer":
                        if not self._is_integer(param_value):
                            self._warning_parameter_value_type_error(
                                "Video Ingestion", "Algorithm", param_name, param_value, value_type)
                            return 
                        else:
                            param_value = int(param_value)

                    elif value_type == "Floating":
                        if not self._is_float(param_value):
                            self._warning_parameter_value_type_error(
                                "Video Ingestion", "Algorithm", param_name, param_value, value_type)
                            return
                        else:
                            param_value = float(param_value)

                    else:
                        param_value = str(param_value)
                
                # ComboBox
                else:
                    if wgt_value_type.currentText() == "True":
                        param_value = True
                    else:
                        param_value = False

                # Add the key/value pair
                udf[param_name] = param_value

            udfs.append(udf)
        module_config["udfs"] = udfs

        # ------------------------------- config: max_workers ------------------------------- #
        max_workers = 4
        if self.ckbox_ingestion_algorithm_udf.isChecked():
            max_workers_str = self.le_ingestion_algorithm_udf_max_workers.text()
            if self._is_integer(max_workers_str) and int(max_workers_str) > 0:
                max_workers = int(max_workers_str)
        module_config["max_workers"] = max_workers
        
        # Add config part
        total_configuration["config"] = module_config

        # ------------------------------- interfaces ------------------------------- #
        module_interfaces = {}
        # ------------------------------- interfaces: servers ------------------------------- #
        servers = []
        server = {}
        if self.cbbox_ingestion_interface_server_type.currentText() == "TCP":
            # Make sure the endpoint of tcp is not an empty string
            server_endpoint = self.le_ingestion_interface_server_endpoint_tcp.text()
            if self.le_ingestion_interface_server_endpoint_tcp.text() == "":
                self._warning_parameter_missing_value("Video Ingestion", "Interface", "[Server]EndPoint")
                return
            # TODO: validate the endpoint
            server["Type"] = "zmq_tcp"
            server["EndPoint"] = server_endpoint
        else:
            server["Type"] = "zmq_ipc"
            server["EndPoint"] = self.le_ingestion_interface_server_endpoint_ipc.text()
        server["Name"] = "default"
        server["AllowedClients"] = ["*"]
        servers.append(server)
        # Add servers to module interfaces
        module_interfaces["Servers"] = servers
        
        # ------------------------------- interfaces: publishers ------------------------------- #
        publishers = []
        publisher = {}
        if self.cbbox_ingestion_interface_publisher_type.currentText() == "TCP":
            # Make sure the endpoint of tcp is not an empty string
            publisher_endpoint = self.le_ingestion_interface_publisher_endpoint_tcp.text()
            if self.le_ingestion_interface_publisher_endpoint_tcp.text() == "":
                self._warning_parameter_missing_value("Video Ingestion", "Interface", "[Publisher]EndPoint")
                return
            # TODO: validate the endpoint
            publisher["Type"] = "zmq_tcp"
            publisher["EndPoint"] = publisher_endpoint
        else:
            publisher["Type"] = "zmq_ipc"
            publisher["EndPoint"] = self.le_ingestion_interface_publisher_endpoint_ipc.text()
        publisher["Name"] = "default"
        publisher["Topics"] = [self.ingestion_topic_name]
        publisher["AllowedClients"] = ["*"]
        publishers.append(publisher)
        # Add publishers to module interfaces
        module_interfaces["Publishers"] = publishers

        # Add interfaces part
        total_configuration["interfaces"] = module_interfaces

        # Make sure the udfs source folder exists
        # if self.ckbox_ingestion_algorithm_udf.isChecked():
        #     if self.ingestion_udfs_path == "":
        #         QMessageBox.warning(self, "Warning", 
        #             "Please import udfs source folder for \"Video Ingestion\" module!", 
        #             QMessageBox.Ok)
        #         return
        #     elif not os.path.exists(self.ingestion_udfs_path):
        #         QMessageBox.warning(self, "Warning", 
        #             "The udfs source folder for \"Video Ingestion\" module\n, \"{}\", does not exist.".format(self.ingestion_udfs_path), 
        #             QMessageBox.Ok)
        #         return

        return total_configuration

    def _generate_analytics_config(self):
        # Contain two parts, config and interfaces
        total_configuration = {}

        # ------------------------------- config ------------------------------- #
        module_config = {}
        # ------------------------------- config: encoding ------------------------------- #
        encoding = {"type": "jpeg", "level": 95}
        module_config["encoding"] = encoding

        # ------------------------------- config: queue_size ------------------------------- #
        module_config["queue_size"] = 10

        # ------------------------------- config: udfs ------------------------------- #
        udfs = []
        if not self.ckbox_analytics_algorithm_udf.isChecked():
            module_config["udfs"] = udfs
        else:
            udf = {}
            udf_name = self.le_analytics_algorithm_udf_name.text()
            if udf_name == "":
                self._warning_parameter_missing_value("Video Analytics", "Algorithm", "Name")
                return
            udf["name"] = udf_name
            udf["type"] = self.cbbox_analytics_algorithm_udf_type.currentText().lower()
            for _, (lbl_param_name, wgt_value_type) in self.analytics_udf_added_items.items():
                param_name = lbl_param_name.text()
                # LineEdit
                if isinstance(wgt_value_type, QLineEdit):
                    param_value = wgt_value_type.text()
                    # Make sure the value is not an empty string
                    if param_value == "":
                        self._warning_parameter_missing_value("Video Analytics", "Algorithm", param_name)
                        return
                    
                    # Make sure the type of input data is same as the value of TypeSelector
                    value_type = wgt_value_type.placeholderText().split(":")[-1].strip()
                    if value_type == "Integer":
                        if not self._is_integer(param_value):
                            self._warning_parameter_value_type_error(
                                "Video Analytics", "Algorithm", param_name, param_value, value_type)
                            return 
                        else:
                            param_value = int(param_value)

                    elif value_type == "Floating":
                        if not self._is_float(param_value):
                            self._warning_parameter_value_type_error(
                                "Video Analytics", "Algorithm", param_name, param_value, value_type)
                            return
                        else:
                            param_value = float(param_value)

                    else:
                        param_value = str(param_value)
                
                # ComboBox
                else:
                    if wgt_value_type.currentText() == "True":
                        param_value = True
                    else:
                        param_value = False

                # Add the key/value pair
                udf[param_name] = param_value

            udfs.append(udf)
        module_config["udfs"] = udfs

        # ------------------------------- config: max_workers ------------------------------- #
        max_workers = 4
        if self.ckbox_analytics_algorithm_udf.isChecked():
            max_workers_str = self.le_analytics_algorithm_udf_max_workers.text()
            if self._is_integer(max_workers_str) and int(max_workers_str) > 0:
                max_workers = int(max_workers_str)
        module_config["max_workers"] = max_workers
        
        # Add config part
        total_configuration["config"] = module_config

        # ------------------------------- interfaces ------------------------------- #
        module_interfaces = {}
        # ------------------------------- interfaces: subscribers ------------------------------- #
        subscribers = []
        subscriber = {}
        if self.cbbox_analytics_interface_subscriber_type.currentText() == "TCP":
            subscriber["Type"] = "zmq_tcp"
        else:
            subscriber["Type"] = "zmq_ipc"
        subscriber["EndPoint"] = self.le_analytics_interface_subscriber_endpoint.text()
        subscriber["Name"] = "default"
        subscriber["PublisherAppName"] = self.ingestion_app_name
        subscriber["Topics"] = [self.ingestion_topic_name]
        subscriber["zmq_recv_hwm"] = 50
        subscribers.append(subscriber)
        # Add subscribers to module interfaces
        module_interfaces["Subscribers"] = subscribers
        
        # ------------------------------- interfaces: publishers ------------------------------- #
        publishers = []
        publisher = {}
        if self.cbbox_analytics_interface_publisher_type.currentText() == "TCP":
            # Make sure the endpoint of tcp is not an empty string
            publisher_endpoint = self.le_analytics_interface_publisher_endpoint_tcp.text()
            if self.le_analytics_interface_publisher_endpoint_tcp.text() == "":
                self._warning_parameter_missing_value("Video Analytics", "Interface", "[Publisher]EndPoint")
                return
            # TODO: validate the endpoint
            publisher["Type"] = "zmq_tcp"
            publisher["EndPoint"] = publisher_endpoint
        else:
            publisher["Type"] = "zmq_ipc"
            publisher["EndPoint"] = self.le_analytics_interface_publisher_endpoint_ipc.text()
        publisher["Name"] = "default"
        publisher["Topics"] = [self.analytics_topic_name]
        publisher["AllowedClients"] = ["*"]
        publishers.append(publisher)
        # Add publishers to module interfaces
        module_interfaces["Publishers"] = publishers

        # Add interfaces part
        total_configuration["interfaces"] = module_interfaces

        # Make sure the udfs source folder exists
        # if self.ckbox_analytics_algorithm_udf.isChecked():
        #     if self.analytics_udfs_path == "":
        #         QMessageBox.warning(self, "Warning", 
        #             "Please import udfs source folder for \"Video Analytics\" module!", 
        #             QMessageBox.Ok)
        #         return
        #     elif not os.path.exists(self.analytics_udfs_path):
        #         QMessageBox.warning(self, "Warning", 
        #             "The udfs source folder for \"Video Analytics\" module\n, \"{}\", does not exist.".format(self.analytics_udfs_path), 
        #             QMessageBox.Ok)
        #         return
        
        return total_configuration

    def _generate_imagestore_config(self):
        # Contain two parts, config and interfaces
        total_configuration = {}

        # ------------------------------- config ------------------------------- #
        module_config = {}
        # ------------------------------- config: minio ------------------------------- #
        minio = {}
        minio["accessKey"] = "admin"
        minio["secretKey"] = "admin123"
        minio["ssl"] = "false"

        retention_time_str = self.le_imagestore_minio_retention_time.text()
        if retention_time_str != "" and self._is_float(retention_time_str):
            retention_time = float(retention_time_str)
            if retention_time <= 0:
                retention_time = 1
        else:
            retention_time = 1
        minio["retentionTime"] = "{}h".format(retention_time)

        retention_poll_interval_str = self.le_imagestore_minio_retention_poll_interval.text()
        if retention_poll_interval_str != "" and self._is_float(retention_poll_interval_str):
            retention_poll_interval = float(retention_poll_interval_str)
            if retention_poll_interval <= 0:
                retention_poll_interval = 60
        else:
            retention_poll_interval = 60
        minio["retentionPollInterval"] = "{}s".format(retention_poll_interval)
        # Add minio to module config
        module_config["minio"] = minio
        
        # Add config part
        total_configuration["config"] = module_config

        # ------------------------------- interfaces ------------------------------- #
        module_interfaces = {}
        # ------------------------------- interfaces: subscribers ------------------------------- #
        subscribers = []
        subscriber = {}
        if self.cbbox_imagestore_interface_subscriber_type.currentText() == "TCP":
            subscriber["Type"] = "zmq_tcp"
        else:
            subscriber["Type"] = "zmq_ipc"
        subscriber["EndPoint"] = self.le_imagestore_interface_subscriber_endpoint.text()
        subscriber["Name"] = "default"
        
        publisher_app_name = self.analytics_app_name
        if self.vi_va_mode != "vi_va":
            publisher_app_name = self.ingestion_app_name
        subscriber["PublisherAppName"] = publisher_app_name
        
        publisher_topic_name = self.analytics_topic_name
        if self.vi_va_mode != "vi_va":
            publisher_topic_name = self.ingestion_topic_name
        subscriber["Topics"] = [publisher_topic_name]
        subscribers.append(subscriber)
        # Add subscribers to module interfaces
        module_interfaces["Subscribers"] = subscribers
        
        # ------------------------------- interfaces: servers ------------------------------- #
        servers = []
        server = {}
        if self.cbbox_imagestore_interface_server_type.currentText() == "TCP":
            # Make sure the endpoint of tcp is not an empty string
            server_endpoint = self.le_imagestore_interface_server_endpoint_tcp.text()
            if self.le_imagestore_interface_server_endpoint_tcp.text() == "":
                self._warning_parameter_missing_value("Image Store", "Interface", "[Server]EndPoint")
                return
            # TODO: validate the endpoint
            server["Type"] = "zmq_tcp"
            server["EndPoint"] = server_endpoint
        else:
            server["Type"] = "zmq_ipc"
            server["EndPoint"] = self.le_imagestore_interface_server_endpoint_ipc.text()
        server["Name"] = "default"
        server["AllowedClients"] = ["*"]
        servers.append(server)
        # Add servers to module interfaces
        module_interfaces["Servers"] = servers

        # Add interfaces part
        total_configuration["interfaces"] = module_interfaces
        
        return total_configuration

    def _generate_opcua_config(self):
        # Contain two parts, config and interfaces
        total_configuration = {}

        # ------------------------------- config ------------------------------- #
        module_config = {}

        if self.le_opcua_topics.text():
            topics = self.le_opcua_topics.text().split(",")
            module_config["OpcuaDatabusTopics"] = topics
        else:
            self._warning_parameter_missing_value(EII_MODULE.OPCUA.value, "config", "OpcuaDatabusTopics")
            return

        if self.le_opcua_address.text():
            address = self.le_opcua_address.text()
            module_config["OpcuaExportCfg"] = address
        else:
            self._warning_parameter_missing_value(EII_MODULE.OPCUA.value, "config", "OpcuaExportCfg")
            return
        
        # Add config part
        total_configuration["config"] = module_config

        # ------------------------------- interfaces ------------------------------- #
        module_interfaces = {}
        # ------------------------------- interfaces: subscribers ------------------------------- #
        subscribers = []
        subscriber = {}
        if self.cbbox_opcua_interface_subscriber_type.currentText() == "TCP":
            subscriber["Type"] = "zmq_tcp"
        else:
            subscriber["Type"] = "zmq_ipc"
        subscriber["EndPoint"] = self.le_opcua_interface_subscriber_endpoint.text()
        subscriber["Name"] = "default"
        
        publisher_app_name = self.analytics_app_name
        if self.vi_va_mode != "vi_va":
            publisher_app_name = self.ingestion_app_name
        subscriber["PublisherAppName"] = publisher_app_name
        
        publisher_topic_name = self.analytics_topic_name
        if self.vi_va_mode != "vi_va":
            publisher_topic_name = self.ingestion_topic_name
        subscriber["Topics"] = [publisher_topic_name]
        subscribers.append(subscriber)
        # Add subscribers to module interfaces
        module_interfaces["Subscribers"] = subscribers
        
        # Add interfaces part
        total_configuration["interfaces"] = module_interfaces
        
        return total_configuration

    def _generate_tcp_config(self):
        # Contain two parts, config and interfaces
        total_configuration = {}

        # ------------------------------- config ------------------------------- #
        module_config = {}

        if self.le_tcp_ip.text():
            module_config["tcp_server_ip"] = self.le_tcp_ip.text()
        else:
            self._warning_parameter_missing_value(EII_MODULE.TCP.value, "config", "Server IP")
            return

        if self.le_tcp_port.text():
            module_config["tcp_server_port"] = self.le_tcp_port.text()
        else:
            self._warning_parameter_missing_value(EII_MODULE.TCP.value, "config", "Server Port")
            return
        
        # Add config part
        total_configuration["config"] = module_config

        # ------------------------------- interfaces ------------------------------- #
        module_interfaces = {}
        # ------------------------------- interfaces: subscribers ------------------------------- #
        subscribers = []
        subscriber = {}
        if self.cbbox_tcp_interface_subscriber_type.currentText() == "TCP":
            subscriber["Type"] = "zmq_tcp"
        else:
            subscriber["Type"] = "zmq_ipc"
        subscriber["EndPoint"] = self.le_tcp_interface_subscriber_endpoint.text()
        subscriber["Name"] = "default"
        
        publisher_app_name = self.analytics_app_name
        if self.vi_va_mode != "vi_va":
            publisher_app_name = self.ingestion_app_name
        subscriber["PublisherAppName"] = publisher_app_name
        
        publisher_topic_name = self.analytics_topic_name
        if self.vi_va_mode != "vi_va":
            publisher_topic_name = self.ingestion_topic_name
        subscriber["Topics"] = [publisher_topic_name]
        subscribers.append(subscriber)
        # Add subscribers to module interfaces
        module_interfaces["Subscribers"] = subscribers
        
        # Add interfaces part
        total_configuration["interfaces"] = module_interfaces
        
        return total_configuration
    
    def static_signals_collector(self):
        # Signals for activating switch mode
        self.btn_vi_va.clicked.connect(self.slot_switch_mode)
        self.btn_only_vi.clicked.connect(self.slot_switch_mode)

        # Signals for activating data stream name
        for item in self.all_clickable_wgts:
            item.clicked.connect(self.slot_data_stream_name)

        # Signals for ingestion configuration
        self.cbbox_ingestion_ingestor_source.currentTextChanged.connect(
            self.slot_configuration_ingestion_source_changed)
        self.btn_ingestion_ingestor_open_video_file.clicked.connect(
            self.slot_configuration_ingestion_open_video_file)
        self.btn_ingestion_algorithm_udf_add.clicked.connect(
            self.slot_configuration_ingestion_udf_add)
        self.ckbox_ingestion_algorithm_udf.toggled.connect(
            self.slot_configuration_ingestion_udf_checked)
        self.btn_ingestion_algorithm_import_udf.clicked.connect(
            self.slot_configuration_ingestion_udf_import)
        self.btn_ingestion_algorithm_reset_udf.clicked.connect(
            self.slot_configuration_ingestion_udf_reset)
        self.btn_ingestion_ingestor_2d_camera_tuner.clicked.connect(
            self.slot_configuration_ingestion_2d_camera_tuner)
        self.btn_ingestion_ingestor_realsense_viewer.clicked.connect(
            self.slot_configuration_ingestion_realsense_viewer)
        self.cbbox_ingestion_interface_server_type.currentTextChanged.connect(
            self.slot_configuration_ingestion_interface_changed)
        self.cbbox_ingestion_interface_publisher_type.currentTextChanged.connect(
            self.slot_configuration_ingestion_interface_changed)
        self.cbbox_ingestion_interface_publisher_type.currentTextChanged.connect(
            self.slot_configuration_ingestion_interface_changed)
        # When the type or endpoint of publisher interface changed, 
        # the changes should be synchronized to video analytics or other modules
        self.cbbox_ingestion_interface_publisher_type.currentTextChanged.connect(
            self.slot_configuration_ingestion_interface_publisher_changed)
        self.le_ingestion_interface_publisher_endpoint_tcp.textChanged.connect(
            self.slot_configuration_ingestion_interface_publisher_changed)

        # Signals for analytics configuration
        self.btn_analytics_algorithm_udf_add.clicked.connect(
            self.slot_configuration_analytics_udf_add)
        self.ckbox_analytics_algorithm_udf.toggled.connect(
            self.slot_configuration_analytics_udf_checked)
        self.btn_analytics_algorithm_import_udf.clicked.connect(
            self.slot_configuration_analytics_udf_import)
        self.btn_analytics_algorithm_reset_udf.clicked.connect(
            self.slot_configuration_analytics_udf_reset)
        self.cbbox_analytics_interface_publisher_type.currentTextChanged.connect(
            self.slot_configuration_analytics_interface_changed)
        # When the type or endpoint of publisher interface changed, 
        # the changes should be synchronized to video analytics or other modules
        self.cbbox_analytics_interface_publisher_type.currentTextChanged.connect(
            self.slot_configuration_analytics_interface_publisher_changed)
        self.le_analytics_interface_publisher_endpoint_tcp.textChanged.connect(
            self.slot_configuration_analytics_interface_publisher_changed)

        # Signals for imagestore configuration
        self.ckbox_enable_image_store.toggled.connect(
            self.slot_configuration_enable_image_store_checked)
        self.cbbox_imagestore_interface_server_type.currentTextChanged.connect(
            self.slot_configuration_imagestore_interface_changed)
    
        # Signals for opcua configuration
        self.ckbox_enable_opcua.toggled.connect(
            self.slot_configuration_enable_opcua_checked)

        # Signals for tcp configuration
        self.ckbox_enable_tcp.toggled.connect(
            self.slot_configuration_enable_tcp_checked)
